RPC调用协议
有一个服务器对外提供若干个可被 RPC 调用的函数。每个函数有一个编号、一个函数名和一串参数类型描述(每个参数类型为 i 表示 4 字节整数,或 s 表示字符串)。现在给定函数表和一个以十六进制字符串表示的 RPC 数据流(可能包含多条 RPC 调用连在一起),要求把数据流解析为可读的函数调用格式并打印出来。
数据流的具体编码规则如下:
-
整个 RPC 数据流用大写十六进制字符表示(字符集合
0-9和A-F),每个字节用两个十六进制字符表示。 -
每条 RPC 调用的二进制布局为:
-
1 字节:函数编号(function id)。
-
紧跟着 为该函数所有字符串参数(
s)分别给出长度的字节 —— 即若函数定义中有 k 个s,在本条调用中会紧接着有 k 个字节,分别表示每个字符串参数的长度(以字节计),顺序与参数定义中s的出现顺序一致。 -
然后按参数顺序依次放置参数的具体数据:
- 若参数类型为
i,占 4 字节,采用 大端(高位字节在前)表示一个整数,输出时按十进制打印。 - 若参数类型为
s,占len字节(前面读到的对应长度),该len字节就是字符串的原始字节。
-
-
假设每个函数至少有一个参数。
-
数据流中可能包含多条 RPC 调用,解析应从头到尾依次解析直到十六进制流结束。
输入说明
- 第一行:正整数
nr_func,表示服务器提供的函数个数。 - 接下来
nr_func行,每行描述一个函数,格式为:
func_id func_name func_args
func_id:整数(十进制),范围 0 ~ 255(占 1 字节)。func_name:字符串(函数名,无空格)。func_args:只包含字符i和s的字符串,表示参数类型的顺序,例如"isi"表示第 1 个参数是字符串,第 2 个是整数,第 3 个是字符串。
- 最后一行:一长串十六进制字符(只含
0-9和A-F,且为偶数长度),表示一个或多个 RPC 调用的串联编码。
输出说明
-
依次输出数据流中每条 RPC 的可读形式,格式:
func_name(arg1,arg2,...)- 对于整型参数
i:按十进制直接输出,例如123。 - 对于字符串参数
s:输出双引号包裹的该字符串对应的十六进制子串(不进行字符解码),例如"6162"。
- 对于整型参数
-
参数之间用英文逗号
,分隔。函数调用之间连续输出(不另加额外空格或换行)。
测试用例
输入:
2
1 concat ss
2 add ii
0102036162414243020000000100000002
输出:
concat("6162","414243")add(1,2)
参考答案
#include <iostream>
#include <iterator>
#include <string>
#include <cassert>
#include <unordered_map>
#include <vector>
#include <queue>
inline int DecodeHex(const std::string& data, int pos, int bytes) {
int result = 0;
int len = bytes * 2;
for (int i = pos; i < pos + len; ++i) {
if ('0' <= data[i] && data[i] <= '9') {
result = result * 16 + (data[i] - '0');
} else if ('A' <= data[i] && data[i] <= 'F') {
result = result * 16 + (data[i] - 'A' + 10);
} else {
assert(0);
}
}
return result;
}
inline int ReadByte(std::string& data, int& pos) {
auto r = DecodeHex(data, pos, 1);
pos += 1 * 2;
return r;
}
inline int ReadInt(std::string& data, int& pos) {
auto r = DecodeHex(data, pos, 4);
pos += 4 * 2;
return r;
}
inline std::string ReadString(std::string& data, int str_len, int& pos) {
auto r = data.substr(pos, str_len * 2);
pos += str_len * 2;
return r;
}
int main() {
int nr_func; // 服务端可供客户端调用的函数个数
std::cin >> nr_func;
// 函数编号,函数命,参数定义
std::unordered_map<int, std::string> func_name_map;
std::unordered_map<int, std::string> func_args_map;
std::unordered_map<int, int> nr_func_str_args_map;
for (int i = 0; i < nr_func; ++i) {
int func_id;
std::string func_name;
std::string func_args;
std::cin >> func_id >> func_name >> func_args;
for (char arg_type : func_args) {
if (arg_type == 's') {
++nr_func_str_args_map[func_id];
}
}
func_name_map[func_id] = std::move(func_name);
func_args_map[func_id] = std::move(func_args);
}
// 16进制表示的rpc数据:
// 1. 可能含有多条rpc数据
// 2. 整数采用大端表示(整数高位字节排在前面)
// 3. 每个函数最少有一个参数
std::string input_data;
std::cin >> input_data;
int i = 0;
while (i < input_data.size()) {
// 读取一字节的rpc编号
int func_id = ReadByte(input_data, i);
// 获取到函数名和参数定义
const std::string& func_name = func_name_map[func_id];
const std::string& func_args = func_args_map[func_id];
int nr_func_str_args = nr_func_str_args_map[func_id];
std::queue<int> str_lens;
for (int j = 0; j < nr_func_str_args; ++j) {
str_lens.push(ReadByte(input_data, i));
}
std::cout << func_name << "(";
for (int j = 0; j < func_args.size(); ++j) {
char arg_type = func_args[j];
if (arg_type == 'i') {
int v = ReadInt(input_data, i);
std::cout << v;
} else if (arg_type == 's') {
// 字符串长度
int str_len = str_lens.front();
str_lens.pop();
std::string str = ReadString(input_data, str_len, i);
std::cout << "\"" << str << "\"";
} else {
assert(0);
}
if (j != func_args.size() - 1) {
std::cout << ",";
}
}
std::cout << ")";
}
}
海域寻宝
小明在一张 height × width 的海域网格地图上寻宝。网格上的每个格子有一个海拔高度(非负整数)。海面初始高度为 0,每小时上升 1(第 t 小时海面高度为 t)。海面上升是整体一致的,不受地形阻挡。
开始时,小明会处于一个起点,并且他知道宝藏位置在哪里。小明可以在四个正交方向(上下左右)移动,每次移动到相邻格子。只有格子被海绵淹没时(即格子的海拔 ≤ 当前海面高度时),该格子才可被通过 。也就是说,必须等到海水淹没起点、终点以及起点到终点路径上的每个格子后,小明才能游过去并取得宝藏。
请问:小明至少要等待几个小时,才能取到宝藏?
输入描述
- 第一行两个正整数:
height(行数)和width(列数)。 - 第二行:起点坐标
start_i start_j(1 ≤ start_i ≤ height,1 ≤ start_j ≤ width)。 - 第三行:终点坐标
target_i target_j(1 ≤ target_i ≤ height,1 ≤ target_j ≤ width)。 - 接下来
height行,每行width个非负整数,表示每个格子的海拔高度a[i][j]。
输出描述
输出一个非负整数,表示能取回宝藏的最早小时数 t。
测试用例
输入:
3 3
1 1
3 3
0 2 2
1 3 4
2 2 1
输出:
2
解释:
一种满足条件的路径是 (1,1)->(2,1)->(3,1)->(3,2)->(3,3),对应高度 [0,1,2,2,1],这条路径上的最大海拔高度为 2,因此最早时间为 t = 2。
二分答案+BFS验证
直接遍历枚举所有可能的t,然后用DFS/BFS验证是否存在通路,只能通过大约60%的用例,之后便会超时。
可以采用二分答案来替代暴力枚举,加速查找过程,经测试可以通过100%的用例。
#include <iostream>
#include <istream>
#include <vector>
#include <queue>
#define IN_RANGE(x, l, r) ((l) <= (x) && (x) < (r))
using Map = std::vector<std::vector<int>>;
int height, width;
int start_i, start_j;
int target_i, target_j;
std::vector<std::pair<int, int>> dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
std::vector<std::vector<bool>> vis;
bool Check(Map& map, int t) {
std::queue<std::pair<int, int>> q;
for (int i = 0; i < height; ++i) {
for (int j = 0; j < width; ++j) {
vis[i][j] = false;
}
}
q.push({start_i, start_j});
vis[start_i][start_j] = true;
while (!q.empty()) {
auto [cur_i, cur_j] = q.front();
q.pop();
if (cur_i == target_i && cur_j == target_j) {
return true;
}
for (auto [d_i, d_j] : dirs) {
int next_i = cur_i + d_i;
int next_j = cur_j + d_j;
if (IN_RANGE(next_i, 0, height) &&
IN_RANGE(next_j, 0, width) &&
(map[next_i][next_j] - t) <= 0 &&
!vis[next_i][next_j]) {
q.push({next_i, next_j});
vis[next_i][next_j] = true;
}
}
}
return false;
}
int main() {
std::ios_base::sync_with_stdio(true);
std::cin.tie(NULL);
std::cout.tie(NULL);
std::cin >> height >> width;
std::cin >> start_i >> start_j;
--start_i, --start_j;
std::cin >> target_i >> target_j;
--target_i, --target_j;
// map[i][j] 海拔高度
int max_time = 0;
Map map(height, std::vector<int>(width));
for (int i = 0; i < height; ++i) {
for (int j = 0; j < width; ++j) {
std::cin >> map[i][j];
max_time = std::max(max_time, map[i][j]);
}
}
vis.assign(height, std::vector<bool>(width));
// 初始水平面0,每小时上涨1。水面上涨不受周围障碍物阻碍。
// 只有当水面涨到同时淹没主人公所处位置、宝藏位置,以及主人公到宝藏之前的路径时,才能取回宝藏。
int u = std::max(map[start_i][start_j], map[target_i][target_j]);
int v = max_time + 1;
int ans = 0;
// 二分答案:
// 1. 如果在当前t的约束条件下存在通路,那么我们进一步缩小二分的右区间,探索是否存在更优化的t。
// 2. 反之,则应该扩大二分的左区间,以确保能够搜索到至少一个有效答案。
while (u < v) {
int t = u + ((v - u) >> 1);
if (Check(map, t)) {
ans = t;
v = t;
} else {
u = t + 1;
}
}
std::cout << ans;
}
改造版Dijsktra
此题是一个经典的"瓶颈路问题"。此类问题根据优化目标的不同,一般有两大类型:
- 最小化最大值 (Minimax Path Problem) : 在所有从起点到终点的路径中,找到一条路径,使得该路径上“最难走”的一段(即权重最大的边或点)的权重尽可能小。这正是本题所属的类型,你需要最小化路径上的最高海拔。
- 最大化最小值 (Maximin Path Problem) : 在所有从起点到终点的路径中,找到一条路径,使得该路径上“最容易走”的一段(即权重最小的边)的权重尽可能大。这个问题也常被称为“最宽路径问题”(Widest Path Problem),可以想象成在网络中传输数据,寻找一条路径使其最小带宽尽可能大。
不难理解,"瓶颈路问题"与"最短路径问题"存在类似之处,都满足"最优子结构"和"贪心选择"性质。更进一步地,对于单源瓶颈路问题,我们可以直接使用——只需要对dist数组的定义和松弛操作进行修改即可。
#include <iostream>
#include <vector>
#include <queue>
#include <array>
#define IN_RANGE(v, l, r) ((l) <= (v) && (v) < (r))
constexpr int INF = std::numeric_limits<int>::max();
std::vector<std::pair<int, int>> dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
struct Node {
int v;
int i;
int j;
bool operator>(const Node& other) const {
return v > other.v;
}
};
using Map = std::vector<std::vector<int>>;
int Dijsktra(const Map& map, int height, int width, int start_i, int start_j, int target_i, int target_j) {
std::vector<std::vector<int>> dist(height, std::vector<int>(width, INF));
std::priority_queue<Node, std::vector<Node>, std::greater<Node>> pq; // 最小堆
// 初始化起点
dist[start_i][start_j] = map[start_i][start_j];
pq.push({
.v = dist[start_i][start_j],
.i = start_i,
.j = start_j
});
while (!pq.empty()) {
Node cur = pq.top();
pq.pop();
if (cur.v > dist[cur.i][cur.j]) {
continue;
}
// 松弛操作
for (auto [di, dj] : dirs) {
int next_i = cur.i + di;
int next_j = cur.j + dj;
if (IN_RANGE(next_i, 0, height) && IN_RANGE(next_j, 0, width)) {
int next_v = std::max(dist[cur.i][cur.j], map[next_i][next_j]);
if (next_v < dist[next_i][next_j]) {
dist[next_i][next_j] = next_v;
pq.push({
.v = next_v,
.i = next_i,
.j = next_j
});
}
}
}
}
return dist[target_i][target_j];
}
int main() {
std::ios_base::sync_with_stdio(true);
std::cin.tie(NULL);
std::cout.tie(NULL);
int height, width;
int start_i, start_j, target_i, target_j;
std::cin >> height >> width;
std::cin >> start_i >> start_j;
--start_i, --start_j;
std::cin >> target_i >> target_j;
--target_i, --target_j;
// map[i][j] 海拔高度
int max_time = 0;
Map map(height, std::vector<int>(width));
for (int i = 0; i < height; ++i) {
for (int j = 0; j < width; ++j) {
std::cin >> map[i][j];
max_time = std::max(max_time, map[i][j]);
}
}
std::cout << Dijsktra(map, height, width, start_i, start_j, target_i, target_j);
}
BOSS的虚弱状态
你是一名手握n种技能的游侠,正在准备与你的队友小明共同迎击一个总血气值为hp的大BOSS。
已知该BOSS的气血值处于闭区间[LowerHp, UpperHp](0<LowerHp<UpperHp<Hp<=1000)时,会进入虚弱状态。此时再由你的队友小明对它发起最终的致命一击。而让BOSS进入虚弱状态的重任,则由你承担。
请问:通过发动若干次技能(可以重复使用同一种技能),你能让BOSS进入虚弱状态吗?如果可以,至少要发动多少次技能,才能让BOSS进入虚弱状态?
输入说明
- 第一行:测试用例数量(正整数)。
- 每个测试用例:
- 第一行三个整数
hp LowerHp UpperHp(0 < LowerHp < UpperHp < hp ≤ 1000)。 - 第二行一个整数
n(1 ≤ n ≤ 100),表示技能数量。 - 第三行
n个正整数d1 d2 ... dn,其中di表示第i个技能每次能减少的气血值(1 ≤ di ≤ 1000)。技能可以任意次数使用(包括 0 次)。
- 第一行三个整数
输出说明
对每个测试用例输出一行,表示让 BOSS 进入虚弱状态所需的最少技能次数。如果无法将 BOSS 的气血降到 [LowerHp, UpperHp] 区间内,输出 0。
测试用例
输入
3
10 3 5
2
3 4
7 3 4
1
5
20 13 15
3
4 3 8
输出
2
0
2
说明:
- 样例 1: 初始
hp = 10,目标区间[3,5],技能为3和4。例如使用两次3伤害:10 -> 7 -> 4,4 在区间内,攻击次数为2(这是最少的)。输出2。 - 样例 2: 初始
hp = 7,目标区间[3,4],技能为单个5。任何连续使用5都会使血量变为7 -> 2(或直接为 2),无法落在[3,4],因此输出0。 - 样例 3:
hp = 20,目标[13,15],技能4,3,8。例如20 -> 16(用 4)->13(用 3),共2次,可以达标,且无法用 1 次达标,因此最少为2。
暴力枚举DP
由于题目中的数据量非常小,直接暴力DP枚举就能过了。
#include <iostream>
#include <vector>
#include <string>
#include <cassert>
#define INF (1<<30)
int main() {
int t;
std::cin >> t;
while (t-- > 0) {
int hp, lower_hp, upper_hp;
// 0<LowerHp<UpperHp<Hp<=1000
std::cin >> hp >> lower_hp >> upper_hp;
int n; // 技能数
std::cin >> n;
// d[i] 第i个技能可以减少的气血值
std::vector<int> d(n);
for (int i = 0; i < n; ++i) {
std::cin >> d[i];
}
// dp[i][j] 使用前i种技能,将hp降低到j,所需要消耗的最小技能数
std::vector<std::vector<int>> dp(n + 1, std::vector<int>(hp + 1, INF));
// 有0种技能,将hp降到hp(即什么事情都没发生),只需要发动0次技能
dp[0][hp] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 0; j <= hp; ++j) {
// 不使用当前技能
dp[i][j] = dp[i - 1][j];
// 使用当前技能
// 暴力枚举发动若干次第i种技能前,boss血量的可能值
for (int prev_hp = hp; prev_hp >= j; --prev_hp) {
int curr_hp = prev_hp;
int cnt = 0;
// 假设boss的血量已下降为prev_hp,
// 模拟在此基础上对boss发动若干次第i种技能。
while (curr_hp >= j) {
// 如果检测发现,经过发动cnt次第i种技能后,
// boss的血量可以由prev_hp下降到j,
// 则尝试更新dp[i][j]。
if (curr_hp == j) {
dp[i][j] = std::min(dp[i - 1][prev_hp] + cnt, dp[i][j]);
break;
}
curr_hp -= d[i - 1];
++cnt;
}
}
}
}
int result = INF;
for (int j = lower_hp; j <= upper_hp; ++j) {
result = std::min(result, dp[n][j]);
}
if (result < INF) {
std::cout << result << std::endl;
}
else {
std::cout << 0 << std::endl;
}
}
}
完全背包问题
本题是一道类完全背包问题(n种物品都可以使用无限次),且优化目标为在达到目标价值的前提下,使得拿的物品总数尽可能地少。
这听上去是不是与leetcode上的"零钱兑换"这道题很像?
是的,我们完全可以把本题转换成"零钱兑换"这道经典面试题来解答!
#include <iostream>
#include <vector>
#include <string>
#include <cassert>
#define INF (1<<30)
int main() {
int t;
std::cin >> t;
while (t-- > 0) {
int hp, lower_hp, upper_hp;
// 0<LowerHp<UpperHp<Hp<=1000
std::cin >> hp >> lower_hp >> upper_hp;
int n; // 技能数
std::cin >> n;
// d[i] 第i个技能可以减少的气血值
std::vector<int> d(n);
for (int i = 0; i < n; ++i) {
std::cin >> d[i];
}
// dp[i] 对BOSS造成i点伤害,至少需要发动几次技能
std::vector<int> dp(hp + 1, INF);
dp[0] = 0;
for (int cur_d : d) {
for (int t = cur_d; t <= hp; ++t) {
dp[t] = std::min(dp[t], dp[t - cur_d] + 1);
}
}
// 遍历BOSS虚弱状态区间,找出使BOSS进入虚弱状态的最少技能数
int ans = INF;
for (int target = hp - upper_hp; target <= hp - lower_hp; ++target) {
ans = std::min(ans, dp[target]);
}
std::cout << ((ans >= INF) ? 0 : ans) << std::endl;
}
}
BFS
此外本题还可以转换成图的最短路径问题来求解。这也是校招笔试中很常见的一个考点和解题技巧。
比如,我们可以将BOSS的不同血量状态,看作图中的一个个不同的节点。通过释放1次技能,我们就可以从图中HP值较大的节点向HP值更小的节点转移。
本题要求的优化目标是释放技能次数最少,这意味着假如我们用图中相邻两个节点之间的那条边代表"释放一次技能",那么本题就转换成了一个从源节点(BOSS的起始HP)到目标节点(BOSS处在虚弱状态的HP)的单源最短路问题。
更进一步地,由于本题图中所有的边的长度都为1(释放1次技能即可从一个节点/HP转换到另一个节点/HP),我们可以很方便地使用BFS算法进行求解。
#include <iostream>
#include <vector>
#include <string>
#include <cassert>
#include <queue>
#define INF (1<<30)
int main() {
int t;
std::cin >> t;
while (t-- > 0) {
int hp, lower_hp, upper_hp;
// 0<LowerHp<UpperHp<Hp<=1000
std::cin >> hp >> lower_hp >> upper_hp;
int n; // 技能数
std::cin >> n;
// d[i] 第i个技能可以减少的气血值
std::vector<int> d(n);
for (int i = 0; i < n; ++i) {
std::cin >> d[i];
}
std::queue<int> q;
std::vector<int> dist(hp + 1, INF);
q.push(hp);
dist[hp] = 0;
while (!q.empty()) {
int cur_hp = q.front();
q.pop();
for (int cur_d : d) {
int next_hp = cur_hp - cur_d;
if (next_hp >= 0 && dist[next_hp] == INF) {
dist[next_hp] = dist[cur_hp] + 1;
q.push(next_hp);
}
}
}
int ans = INF;
for (int target = lower_hp; target <= upper_hp; ++target) {
ans = std::min(ans, dist[target]);
}
std::cout << ((ans >= INF) ? 0 : ans) << std::endl;
}
}
飞船入侵
在一次联合作战中,你指挥的 n (1 ≤ n ≤ 10^6)名玩家同时对敌方飞船释放能量炮。第 i 个玩家的能量炮在整数时刻 a[i] (0 ≤ a[i] ≤ 10^6)开始生效,持续恰好 keep_time (1 ≤ keep_time ≤ 10^6)个连续的整数时刻(也就是说,它在时刻 a[i], a[i]+1, ..., a[i]+keep_time-1 都会造成伤害)。能量炮在每个被覆盖的整数时刻对飞船造成固定的 b[i](1 ≤ b[i] ≤ 10^6)点伤害。
在任意一个整数时刻,飞船受到的总伤害等于此时刻所有处于生效状态的能量炮的 b[i] 之和。请计算飞船在所有整数时刻中所承受的最大总伤害。
输入说明
-
第一行包含两个整数:
n(玩家数)和keep_time(每个能量炮的持续时长)。 -
接下来
n行,每行包含两个整数a[i]和b[i]:a[i]—— 第i个能量炮开始生效的整数时刻(起始时刻,包含在内)。b[i]—— 第i个能量炮在每个被覆盖时刻造成的伤害值。
输出说明
输出一个整数,表示飞船在任意整数时刻受到的最大总伤害。
测试用例
输入1:
3 4
1 3
2 5
6 2
输出1:
8
输入2:
4 5
0 10
0 20
0 30
0 40
输出2:
100
输入3:
3 1
5 7
2 3
5 10
输出3:
17
暴力解法
只能过60%的测试用例。
性能瓶颈:在每个时间点上,都需要遍历所有玩家的能量炮,计算当前时间点的总伤害值。
#include <iostream>
#include <vector>
int main() {
/* 我方玩家数量,能量炮持续时间 */
int n, keep_time;
std::cin >> n >> keep_time;
/*
* n行,a[i]每个能量炮对飞船造成伤害的开始时间
* b[i] 每个能量炮对飞船造成伤害的伤害值
*/
std::vector<int64_t> a( n );
std::vector<int64_t> b( n );
int max_time = 0;
for ( int i = 0; i < n; ++i ) {
std::cin >> a[i] >> b[i];
max_time = std::max( max_time, a[i] + keep_time - 1 );
}
int64_t ans = 0;
for (int current_time = 0; current_time <= max_time; ++current_time ) {
/* 遍历所有玩家,检查哪些玩家的能量炮处于有效区间 */
int64_t sum = 0;
for ( int player = 0; player < n; ++player ) {
if ( a[player] <= current_time && current_time < a[player] + keep_time ) {
sum += b[player];
}
}
ans = std::max( ans, sum );
}
std::cout << ans;
}
优化解法
事实上,我们不需要在每个时间点都从头计算总的伤害值。t时刻的总伤害值,相较于t-1时刻的总伤害值,只有可能当t时刻有能量炮进入启用状态,或者有能量炮的有效状态结束,才可能发生变化。
据此,我们可以借鉴差分数组/滑动窗口/扫描线的思想,构建更高效的算法。
#include <iostream>
#include <vector>
#include <algorithm>
struct Event {
int time;
int diff;
};
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
std::cout.tie(NULL);
int n, keep_time;
std::cin >> n >> keep_time;
std::vector<Event> events;
for (int i = 0; i < n; ++i) {
Event start_event;
std::cin >> start_event.time >> start_event.diff;
Event end_event = {
.time = start_event.time + keep_time,
.diff = -start_event.diff,
};
events.emplace_back(start_event);
events.emplace_back(end_event);
}
// 对所有事件,按时间先后排序
std::sort(events.begin(), events.end(), [](const auto& a, const auto& b) -> bool {
return a.time < b.time;
});
int64_t ans = 0;
int64_t sum = 0;
for (int i = 0; i < events.size(); ++i) {
sum += events[i].diff;
if (i + 1 == events.size() || events[i].time < events[i + 1].time) {
ans = std::max(ans, sum);
}
}
std::cout << ans;
}