
快递单号
多多在快递公司负责快递单号录入工作,这些单号有严格的格式要求:
快递单号由3部分组成:2位大写字母(A~Z) + 6位数字 + 1位校验位
校验位计算规则:取前8位(2 字母 + 6 数字)中每个字符的ASCII码之和,对26取余后,加上A的ASCII码,得到的字符即为校验位
现在有一批可能存在校验位错误的单号,请你编写程序:
- 若单号格式正确且校验位正确,返回原单号
- 若前 8 位格式正确但校验位错误,返回修复后(校正校验位)的单号
- 若前 8 位格式错误(非 2 字母 + 6 数字)或快递单号不满足格式要求,返回字符串Invalid
输入描述
共一行,一个字符串(1<=长度<=1024),表示待校验的快递单号
输出描述
共一行,一个字符串,表示修复后的快递单号,若无法修复则返回字符串Invalid
测试用例
示例 1
输入
AB123456C
输出
AB123456Y
说明:
前 8 位字符:A(ASCII码=65)、B(ASCII码=66)、1(ASCII码=49)、2(ASCII码=50)、3(ASCII码=51)、4(ASCII码=52)、5(ASCII码=53)、6(ASCII码=54)
总和:65+66+49+50+51+52+53+54 = 440,440 % 26 = 24,24 + 65 (A的ASCII码) = 89,对应字符Y
因此该单号校验位错误,正确单号应为AB123456Y
示例 2
输入
AC123456Z
输出
AC123456Z
说明:
前 8 位字符:A(ASCII码=65)、C(ASCII码=67)、1(ASCII码=49)、2(ASCII码=50)、3(ASCII码=51)、4(ASCII码=52)、5(ASCII码=53)、6(ASCII码=54)
总和:65+67+49+50+51+52+53+54 = 441,441 % 26 = 25,25 + 65 (A的ASCII码) = 90,对应字符Z
因此该单号校验位正确,直接返回单号AC123456Z
示例 3
输入
AC123M56Z
输出
Invalid
说明:
快递单号由3部分组成:2位大写字母 + 6位数字 + 1位大写字母校验位,不满足规则,直接返回Invalid
参考答案
签到题,秒了。
C++
#include <iostream>
#include <limits>
#include <string>
int main() {
std::string input_data;
std::cin >> input_data;
if (input_data.size() != 9) {
std::cout << "Invalid" << std::endl;
return 0;
}
// 检查前8位格式
int letter_cnt = 0;
int number_cnt = 0;
int sum = 0;
for (int i = 0; i < 8; ++i) {
if ('A' <= input_data[i] && input_data[i] <= 'Z') {
++letter_cnt;
} else if ('0' <= input_data[i] && input_data[i] <= '9') {
++number_cnt;
}
sum += input_data[i];
}
if (letter_cnt != 2 || number_cnt != 6) {
std::cout << "Invalid" << std::endl;
return 0;
}
int checker = input_data[8];
int target_checker = (sum % 26) + 'A';
input_data[8] = target_checker;
std::cout << input_data;
}
圣诞树
圣诞节快到了,有一棵挂满彩灯的二叉树,需要你来按照图纸装饰。彩灯有5种颜色变化,分别用1-5 表示。1表示 红色, 2表示黄色, 3表示蓝色, 4 表示紫色, 5 表示绿色。
每个节点都一个颜色控制器,每按一下都会产生一个控制信号。控制信号将从当前节点出发向下传递,将当前节点的彩灯以及以当前节点为根节点的子树上的所有节点,切换到下一个颜色( 红 -> 黄 -> 蓝 -> 紫 -> 绿 -> 红 ...) 循环切换。
给定二叉树的初始状态 initial 和 目标状态 target, 两者都以层序遍历产出的一维数组表示。数组元素对应对应位置节点的颜色,0表示该节点没有彩灯。
请给出从initial状态切换至target状态需要的最少控制器点击次数。
注意:
- 控制器按一下所产生的控制信号,不只影响当前节点,也会影响以当前节点为根节点的子树上所有节点切换到下一个颜色(最终不一定是同一个颜色)。
- 特别地,假设子树上的某个节点X上没有彩灯,则祖先节点处发出的控制信号将不会继续传递给X的后代节点。
输入描述
第一行输入为一个整数n, 代表inital 和 target 数组的大小。
第二行输入为n个整数,代表inital数组。
第三行输入为n个整数,代表target数组。
其他:
- 如果 initial[i] == 0, 则 target[i] 也一定为0。
- 1 <=initial.length <= 106
输出描述
一个整数,表示最少点击次数
测试用例
示例 1
输入
5
1 2 3 0 1
2 3 1 0 2
输出
3
示例 2
输入
7
1 2 3 1 2 3 1
3 1 2 3 1 2 1
输出
10
参考答案
借助反证法不难证明,从根节点出发,从上到下依次调整所有彩灯的颜色,是一种可行的点击总次数最少的策略。
不存在比该策略所需点击次数还少的策略。因为假如存在的话,不难得出二叉树上至少存在一个节点的彩灯未被调整至目标颜色的结论,这显然是荒谬的。
代码直接用递归来写就可以了。一是要注意对调整子树根节点颜色对它的后代节点的影响,二是,模运算的表达式别写错了。
要点:
- 注意对调整子树根节点颜色对它的后代节点的影响
- 注意如何计算将某个节点从当前颜色调整至目标颜色所需的点击次数
C++
#include <iostream>
#include <vector>
// i => 子节点2*i+1,2*i+2
std::vector<int> tree;
std::vector<int> target;
int Solve(int root, int inherited) {
if (root >= tree.size()) {
return 0;
}
int left = 2 * root + 1;
int right = 2 * root + 2;
// 此处没有彩灯
if (tree[root] == 0) {
return Solve(left, 0) + Solve(right, 0);
} else {
int current = (tree[root] + inherited - 1) % 5 + 1;
int delta = (target[root] - current + 5) % 5;
return Solve(left, delta + inherited) + Solve(right, delta + inherited) + delta;
}
}
int main() {
int n;
std::cin >> n;
tree.resize(n);
target.resize(n);
for (int i = 0; i < n; ++i) {
std::cin >> tree[i];
}
for (int i = 0; i < n; ++i) {
std::cin >> target[i];
}
std::cout << Solve(0, 0);
}
魔法学院
多多进入了魔法学院学习,学院有 n
门不同的魔法课程,每门课程都有其独特的属性:
power[i]
:学习这门课程能提升的魔法强度mana[i]
:学习这门课程需要消耗的法力值
学院的教学楼有 m
层,每层有不同的环境加成系数 bonus[j]
(1 ≤ bonus[j] ≤ 3)。
多多总共有 M
点初始法力值。
特殊规则:
- 顺序学习:多多必须按顺序学习课程(必须先学课程1,再学课程2,以此类推)。
- 楼层绑定:每门课程只能在某一层完整学习,不能跨层。
- 强度加成:在第
j
层学习第i
门课程时,获得的实际魔法强度为power[i] × bonus[j]
。 - 法力消耗:在第
j
层学习第i
门课程时,消耗的实际法力值为mana[i] × bonus[j]
。 - 切换代价:
- 多多可以在不同楼层之间切换课程,第一次学习选择楼层没有切换代价, 但每次切换可能会额外消耗楼层高度差的法力值。
- 如果从低楼层切换到高楼层, 比如从1层切换到4层, 消耗3点法力, 如果从高楼层切换到低楼层, 则不会消耗额外的法力
请求出在满足法力值限制(总法力消耗不超过 M
)的条件下,多多能获得的最大魔法强度总和(无需学完所有课程)。
输入描述
第一行三个整数 n
, m
, M
(1 ≤ n ≤ 100, 1 ≤ m ≤ 5, 1 ≤ M ≤ 1000) 第二行 n
个整数,表示 power[i]
(1 ≤ power[i] ≤ 100) 第三行 n
个整数,表示 mana[i]
(1 ≤ mana[i] ≤ 100) 第四行 m
个整数,表示 bonus[j]
(1 ≤ bonus[j] ≤ 3)
输出描述
输出一个整数,表示能获得的最大魔法强度总和。如果无法完成任何课程(例如,第一门课程在任何一层学习的法力消耗都超过 M
),则输出 0。
补充说明
对于 20% 的数据:n ≤ 10, 1 ≤ m ≤ 5, 1 ≤ M ≤ 1000
对于 60% 的数据:n ≤ 30, 1 ≤ m ≤ 5, 1 ≤ M ≤ 1000
对于 100% 的数据:1 ≤ n ≤ 100, 1 ≤ m ≤ 5, 1 ≤ M ≤ 1000
示例
示例 1
输入
1 1 5
10
5
2
输出
0
说明:
1门课程,1层楼,初始法力值5 课程强度10,消耗5,楼层加成2
实际消耗 = 5×2 = 10 > 5 (无法学习)
示例 2
输入
2 2 20
5 10
2 3
2 3
输出
45
说明:
-
2门课程,2层楼,初始法力值20
-
课程强度: [5, 10],消耗: [2, 3],楼层加成: [2, 3]
可能的方案:
-
都在1层(加成2): 消耗 = 2×2 + 3×2 = 4+6=10,强度 = 5×2 + 10×2 = 10+20=30
-
都在2层(加成3): 消耗 = 2×3 + 3×3 = 6+9=15,强度 = 5×3 + 10×3 = 15+30=45
-
课程1在1层,课程2在2层: 切换消耗 = 2-1=1,总消耗 = 2×2 + 3×3 + 1 = 4+9+1=14,强度 = 5×2 + 10×3 = 10+30=40
-
课程1在2层,课程2在1层: 切换消耗 = 0,总消耗 = 2×3 + 3×2 = 6+6=12,强度 = 5×3 + 10×2 = 15+20=35
参考答案(高维动态规划)
本题是一个最优化问题,涉及到法力消耗情况、课程、楼层三个状态,优化的目标为让魔法强度总和最大,非常适合使用高维动态规划来解答。
以下是本人编写的代码:
C++
#include <iostream>
#include <vector>
#define INF (1 << 30)
static int dp[1002][102][102];
int main() {
int n, m, M; // 课程数,教学楼的层数,总法力
std::cin >> n >> m >> M;
std::vector<int> power(n); // 学习第i门课程,可以提升的魔法强度
std::vector<int> mana(n); // 学习第i门课程,需要消耗的法力
std::vector<int> bonus(m); // 第i层的环境加成系数
for (int i = 0; i < n; ++i) {
std::cin >> power[i];
}
for (int i = 0; i < n; ++i) {
std::cin >> mana[i];
}
for (int i = 0; i < m; ++i) {
std::cin >> bonus[i];
}
// dp[i][j][k] 总消耗法力为i,在第j层学完第k门课程后,能得到的最大魔法强度总和
// 初始化dp数组
for (int i = 0; i <= M; ++i) {
for (int j = 0; j < m; ++j) {
for (int k = 0; k < n; ++k) {
dp[i][j][k] = -INF;
}
}
}
int ans = 0;
// 先单独计算学习第0门课能获得的最大魔法强度
for (int j = 0; j < m; ++j) {
int cost = bonus[j] * mana[0];
if (cost <= M) {
dp[cost][j][0] = bonus[j] * power[0];
ans = std::max(ans, dp[cost][j][0]);
}
}
for (int i = 0; i <= M; ++i) {
for (int j = 0; j < m; ++j) {
for (int k = 1; k < n; ++k) {
for (int last_j = 0; last_j < m; ++last_j) {
int patch = 0;
// 从低层切换到高层
if (last_j < j) {
patch = j - last_j;
}
int curr_cost = bonus[j] * mana[k] + patch;
int curr_power = bonus[j] * power[k];
if (i - curr_cost >= 0) {
dp[i][j][k] = std::max(dp[i][j][k],
dp[i - curr_cost][last_j][k - 1] + curr_power);
ans = std::max(ans, dp[i][j][k]);
}
}
}
}
}
std::cout << ans;
}
以下是ai生成的一套解答代码,除了dp数组的定义和遍历顺序不一致外,整体思路其实是相通的:
python
n, m, M = map(int, input().split())
power = list(map(int, input().split()))
mana = list(map(int, input().split()))
bonus = list(map(int, input().split()))
# dp[i][j][k] 表示前i门课程,最后一门在第j层,总法力消耗k时的最大魔法强度
# 初始化为-1,表示不可达
dp = [[[-1] * (M + 1) for _ in range(m + 1)] for __ in range(n + 1)]
dp[0][0][0] = 0
ans = 0
for i in range(1, n + 1):
for j in range(1, m + 1):
cur_mana = mana[i-1] * bonus[j-1]
cur_power = power[i-1] * bonus[j-1]
# 确定p的范围:i=1时p可以是0,i>1时p必须是1到m
if i == 1:
p_start, p_end = 0, m
else:
p_start, p_end = 1, m
for p in range(p_start, p_end + 1):
# 计算切换成本
if p == 0:
cost = 0
else:
if p < j:
cost = j - p
else:
cost = 0
# 遍历k0
for k0 in range(M + 1):
if dp[i-1][p][k0] == -1:
continue
total = k0 + cost + cur_mana
if total <= M:
new_val = dp[i-1][p][k0] + cur_power
if new_val > dp[i][j][total]:
dp[i][j][total] = new_val
if new_val > ans:
ans = new_val
print(ans)
错误解法剖析
下面我们再来看一个某ai大模型生成的错误解法。
C++
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
int main() {
int n, m, M;
cin >> n >> m >> M;
vector<int> power(n + 1), mana(n + 1), bonus(m + 1);
for (int i = 1; i <= n; i++) cin >> power[i];
for (int i = 1; i <= n; i++) cin >> mana[i];
for (int i = 1; i <= m; i++) cin >> bonus[i];
// dp[i][j]:学完第 i 门课,且第 i 门课在楼层 j 时的最大强度
// cost[i][j]:对应的最小法力消耗
vector<vector<long long>> dp(n + 1, vector<long long>(m + 1, -1));
vector<vector<long long>> cost(n + 1, vector<long long>(m + 1, LLONG_MAX));
// 初始化:没学任何课程时,在任意楼层强度为 0,消耗为 0
for (int j = 1; j <= m; j++) {
dp[0][j] = 0;
cost[0][j] = 0;
}
long long ans = 0;
for (int i = 1; i <= n; i++) {
for (int j_curr = 1; j_curr <= m; j_curr++) {
// 当前课程 i 在楼层 j_curr 的基础消耗和收益
int base_cost = mana[i] * bonus[j_curr];
int gain = power[i] * bonus[j_curr];
// 尝试从前一门课 i-1 在楼层 j_prev 转移过来
for (int j_prev = 1; j_prev <= m; j_prev++) {
if (dp[i - 1][j_prev] == -1) continue; // 前一个状态不可达
// 切换楼层代价
int switch_cost = 0;
if (j_curr > j_prev) {
switch_cost = j_curr - j_prev;
}
int total_cost = base_cost + switch_cost;
long long new_cost = cost[i - 1][j_prev] + total_cost;
if (new_cost <= M) {
long long new_power = dp[i - 1][j_prev] + gain;
// 如果强度更大,或者强度相同但消耗更小,则更新
if (new_power > dp[i][j_curr] ||
(new_power == dp[i][j_curr] && new_cost < cost[i][j_curr])) {
dp[i][j_curr] = new_power;
cost[i][j_curr] = new_cost;
}
}
}
// 如果当前状态可达,更新答案
if (dp[i][j_curr] != -1) {
if (dp[i][j_curr] > ans) {
ans = dp[i][j_curr];
}
}
}
}
// 还要考虑只学部分课程的情况,但这里 dp[i][j] 已经包含所有 i 的情况
// 因为我们在循环中已经对每个 i 更新了 ans
cout << ans << endl;
return 0;
}
这个解法会在以下的测试用例中得出错误结果
5 5 147
20 2 50 49 30
39 2 22 19 44
2 2 3 3 1
这个case的正确答案应该为273,具体计算过程如下:
- 在第5层学习第1门课,魔法强度=20 法力=39
- 在第4层学习第2门课,魔法强度=20+2*3=26 法力=39+2*3=45
- 在第2层学习第3门课,魔法强度=26+50*2=126 法力=45+22*2=45+44=89
- 在第3层学习第4门课,魔法强度=126+49*3=273 法力=89+19*3+1=147
而上面的这段代码的输出结果为116,显然不是最优解。问题出在哪儿呢?
事实上,对于每一个状态<i, j>,该算法都会在总消耗不突破M的前提下,尝试选择增加魔法强度最大的方案。这种贪心选择看似能帮助我们尽可能地优化魔法强度的总和,但却忽略了被选方案所引起的总法力消耗对后续选择的影响,从而陷入了"局部最优解"的陷阱。
这个错误,与有的同学会尝试利用贪心算法来解决01背包问题(例如在不超过背包总容量的前提下,尽可能地拿更贵重的物品,而完全不考虑它们的重量对后续决策的影响),在本质上是一致的。
大鱼吃小鱼
多多在玩大鱼吃小鱼的游戏,目前有 n 条鱼,编号从 1 到 n 按顺序排列,第 i 条鱼的血量记为 ai 。
该游戏有以下规则:一条鱼只有在它的血量严格大于(不包含等于)它相邻的鱼时,它才能吃掉这条相邻的鱼,并且增加自身血量,增加值等于被吃掉的鱼的血量。如果没有任何一条鱼的血量严格大于它的邻居,则游戏结束。
例如,有 n=5,a=[2,2,3,1,4]。该过程可能如下进行:
首先,第 3 条鱼吃掉第 2 条鱼。第 3 条鱼的血量变为 5,第 2 条鱼被吃掉。
然后,第 3 条鱼吃掉第 1 条鱼(由于第 2 条鱼已被吃掉,他们现在是相邻的)。第 3 条鱼的血量变为 7,第 1 条鱼被吃掉。
接着,第 5 条鱼吃掉第 4 条鱼。第 5 条鱼的血量变为 5,第 4 条鱼被吃掉。
最后,第 3 条鱼吃掉第 5 条鱼(由于第 4 条鱼已被吃掉,他们现在是相邻的)。第 3 条鱼的血量变为 12,第 5 条鱼被吃掉。
请你设计一个程序,用于求解:对于每一条鱼,计算在所有可能的进食顺序中,它被其他鱼吃掉所需的最少次数是多少?如果它不可能被吃掉,则输出 −1。
输入描述
共2行,第一行包含一个正整数n,表示鱼的数量。(1<= n <=10^5)
第2行包含n个正整数: a1,a2,...,an(1<= n <=10^5),表示每条鱼的血量。
测试样例中n条鱼的血量之和不会超过10^10。
输出描述
共1行,每行输出 n 个整数。第 i 个整数表示第 i 条鱼被其他鱼吃掉所需的最少次数;如果不可能被吃掉,则输出 −1。
示例 1
输入
4
3 2 4 2
输出
2 1 2 1
示例 2
输入
3
1 2 3
输出
1 1 -1
示例 3
输入
5
2 2 3 1 1
输出
2 1 -1 1 2
示例 4
输入
5
1 3 4 5 12
输出
1 1 1 1 4
参考答案(暴力DFS)
可以用DFS来暴力枚举所有可能的情况。这种解法只能过大约20%的测试用例。
C++
#include <iostream>
#include <limits>
#include <vector>
int FindLeft(std::vector<int64_t>& fish, int i) {
int j = i - 1;
// 找到左边第一条存活的鱼
while (0 <= j && fish[j] <= 0) {
--j;
}
return (0 <= j && 0 < fish[j] && fish[j] < fish[i]) ? j : -1;
}
int FindRight(std::vector<int64_t>& fish, int i) {
int j = i + 1;
while (j < fish.size() && fish[j] <= 0) {
++j;
}
return (j < fish.size() && 0 < fish[j] && fish[j] < fish[i]) ? j : -1;
}
void Solve(std::vector<int64_t>& fish, int cnt, std::vector<int>& ans) {
for (int i = 0; i < fish.size(); ++i) {
// 如果当前鱼已经被吃掉,则直接跳过
if (fish[i] <= 0) {
continue;
}
// 尝试去吃左边的鱼
int left = FindLeft(fish, i);
if (left != -1) {
int64_t backup = fish[left];
ans[left] = std::min(ans[left], cnt);
fish[left] = 0;
fish[i] += backup;
Solve(fish, cnt + 1, ans);
fish[left] = backup;
fish[i] -= backup;
}
// 尝试去吃右遍的鱼
int right = FindRight(fish, i);
if (right != -1) {
int64_t backup = fish[right];
ans[right] = std::min(ans[right], cnt);
fish[right] = 0;
fish[i] += backup;
Solve(fish, cnt + 1, ans);
fish[right] = backup;
fish[i] -= backup;
}
}
}
int main() {
int n;
std::cin >> n;
std::vector<int64_t> fish(n);
for (int i = 0; i < n; ++i) {
std::cin >> fish[i];
}
std::vector<int> ans(n, std::numeric_limits<int>::max());
Solve(fish, 1, ans);
for (int num : ans) {
if (num == std::numeric_limits<int>::max()) {
std::cout << -1 << " ";
}
else {
std::cout << num << " ";
}
}
}
参考答案(BFS)
利用BFS支持求解最短路径的特性。
C++
#include <iostream>
#include <vector>
#include <set>
#include <queue>
using FishState = std::vector<int64_t>;
int FindLeft(FishState& fish_state, int i) {
int j = i - 1;
// 向左找到第一条存活的鱼
while (0 <= j && fish_state[j] == 0) {
--j;
}
return (0 <= j && fish_state[j] > fish_state[i]) ? j : -1;
}
int FindRight(FishState& fish_state, int i) {
int j = i + 1;
// 向左找到第一条存活的鱼
while (j < fish_state.size() && fish_state[j] == 0) {
++j;
}
return (j < fish_state.size() && fish_state[j] > fish_state[i]) ? j : -1;
}
int Solve(FishState& fish_state, int target_fish) {
std::queue<std::pair<int, FishState>> q;
std::set<FishState> visited;
q.push({0, fish_state});
visited.insert(fish_state);
int steps = 0;
while (!q.empty()) {
auto& [steps, curr_fish_state] = q.front();
if (curr_fish_state[target_fish] == 0) {
return steps;
}
// 遍历curr_fish_state,计算下一步可能转移的状态
for (int i = 0; i < fish_state.size(); ++i) {
// 如果当前鱼已经被吃掉了,跳过
if (curr_fish_state[i] == 0) {
continue;
}
// 当前鱼被左边的鱼吃掉
int left = FindLeft(curr_fish_state, i);
if (left != -1) {
FishState next_fish_state = curr_fish_state;
next_fish_state[left] += next_fish_state[i];
next_fish_state[i] = 0;
if (!visited.contains(next_fish_state)) {
q.push({steps + 1, next_fish_state });
visited.insert(next_fish_state);
}
}
// 当前鱼被右遍的鱼吃掉
int right = FindRight(curr_fish_state, i);
if (right != -1) {
FishState next_fish_state = curr_fish_state;
next_fish_state[right] += next_fish_state[i];
next_fish_state[i] = 0;
if (!visited.contains(next_fish_state)) {
q.push({ steps + 1, next_fish_state });
visited.insert(next_fish_state);
}
}
}
q.pop();
}
return -1;
}
int main() {
int n;
std::cin >> n;
FishState fish_state(n);
for (int i = 0; i < n; ++i) {
std::cin >> fish_state[i];
}
for (int i = 0; i < n; ++i) {
std::cout << Solve(fish_state, i) << " ";
}
}