先说结论:跳房子I这道题,本身并不难,但它是非常经典的“一看就会、一写就错”的题目。很多同学在网上搜到过各种版本,但真正落到华为OD机试的ACM输入输出格式下,用Python、Java、C++分别写对,还是有不少门道的。这篇文章我直接把三种语言的完整实现、边界条件的处理逻辑、我在调试时踩过的坑,全部摊开讲清楚。
1. 题目到底在问什么:先吃透题意再动手
1.1 题目描述与核心规则
跳房子I,也叫“跳房子游戏”或者“房子跳跃”,题目背景是这样的:你玩过跳房子吧,一格一格按顺序跳,但这里的规则不同,输入是一组步长数组,每一步你可以跳任意步数,但有一个关键限制:每跳到一个格子,你只能记录这个格子上的数值,然后你当前位置会跳到这个数值对应的下标位置。
等一下,这个描述可能和你在别处看到的版本不太一样。这里我先还原华为OD机试里比较常见的原题描述:
小明和朋友们玩跳房子游戏,现给定一个非负整数数组,数组中的每个数字代表从当前位置出发可以跳跃的最大长度,也就是说,如果你在第i个位置,数组值为nums[i],那么你下一次跳到的位置是 i + nums[i]。游戏规则要求从下标0开始,跳到最后一个下标(即数组末尾)为止,中途每次跳跃都必须严格递增下标。题目问的是:是否有办法跳到最后一个位置?如果可以,输出最少跳跃次数;如果不可以,输出-1。
但要注意,“跳房子I”在华为OD题库里还有一个变体:不是问“能否到达”,而是问“给定一个目标值,找出数组中两个数的和等于目标值,并返回这两个数的下标组合”——这才是“跳房子I”让人迷惑的地方。
我查了不少资料,结合热搜词里反复出现的“跳房子I”题目标题,以及华为OD机试C卷、D卷的题库目录,确定“跳房子I”在OD机试中的含义是:
输入一个整数数组arr和一个目标值target,从数组中找到两个数,它们的和等于target,输出这两个数的下标。如果有多个组合,输出下标乘积最小的那组(即i*j最小)。题目还会保证恰好存在一个解。
这其实就是LeetCode第一题“两数之和”的变体,但加了一个“下标乘积最小”的筛选条件,而且输出格式要按照题目要求的顺序打印下标,下标从1开始还是从0开始,不同批次题目可能不同。
所以,看到这里你先别急着写代码,第一件事是:确认你拿到的题目版本到底是要“跳跃到达”还是“两数之和”。华为OD机试的题库确实存在同名不同题的情况,“跳房子I”和“跳房子II”通常是同一个系列,I相对简单,II会增加一些限制条件(比如房子编号从1开始,或者要求输出所有组合)。
我下面按照最常见的“两数之和 + 下标乘积最小”版本来精讲,因为这是目前题库里出现频率最高的。如果你拿到的题是“跳跃到达”,思路会完全不一样,我最后也会补充一下区分方法。
1.2 输入输出格式与ACM模式
华为OD机试现在用的是ACM模式,也就是你自己负责读输入、自己打印输出,不能像力扣那样直接写函数返回。这一点非常关键,很多刷惯了力扣的同学第一场机试就挂在这里。
输入格式一般是这样:
7 5 8 2 3 1 9 4 13第一行是数组长度n,第二行是n个整数,第三行是目标值target。输出格式是:
2 5意思是数组下标为2和5的两个数相加等于13。注意,有的批次要求输出的是“房子编号”,也就是下标加1,那么就要输出3 6。还有一种说法是两个数字本身,如输出2 9。我建议你考试时仔细读题目的输出样例,以样例为准。
这里还有一个隐藏考点:多组输入。有些题目会连续给多组数据,你需要用while循环不断读取,直到EOF;有些只给一组。稳妥的做法是写一个能处理多组输入的模板,适配性更强。
2. 解题思路与算法选型:为什么是哈希表
2.1 暴力解法为什么不行
看到两数之和,第一反应肯定是两层循环,把所有组合都试一遍:
for i in range(n): for j in range(i+1, n): if arr[i] + arr[j] == target: ...时间复杂度是O(n²)。如果数组长度n是100,没问题;如果是10000,OJ直接超时;如果是100000,根本跑不动。
华为OD机试的时间限制一般是1秒,C++能抗住的运算量大约在10^8量级,Python大约在10^7量级。如果n=10^4,O(n²)就是10^8,Python几乎必挂。所以暴力解法只能拿到部分分,甚至部分分都拿不到。
这道题最标准的解法是“哈希表 + 单次遍历”,时间复杂度O(n),空间复杂度O(n)。这也是面试官想考察的核心点:能不能想到用空间换时间。
2.2 哈希表方案的核心逻辑
核心思路其实很朴素:我一边遍历数组,一边把已经见过的数存到哈希表里,key是数值,value是下标。每到一个新位置,我算一下target - 当前值,如果这个差值已经在哈希表里,说明找到了两个数。
这里有一个顺序问题:是先查表再存,还是先存再查?答案是先查再存。因为如果先存,当target = 2 * arr[i]时,你会把当前下标和自己匹配上,导致错误结果。比如数组是[1, 2, 3],target是4,遍历到第二个数2时,如果先存再查,就会认为2 + 2 = 4,匹配到下标(1,1),这显然是错的。
所以正确的遍历顺序是:
- 计算
remain = target - arr[i] - 在哈希表中查找
remain - 如果找到了,记录下标对
- 如果没找到,把
arr[i]和下标i存入哈希表
2.3 “下标乘积最小”的筛选是怎么实现的
这就是跳房子I区别于原始两数之和的地方。题目要求如果有多个解,输出下标乘积最小的组合,即i * j最小。
这里需要仔细想一下:在哈希表单次遍历中,我们找到的第一组解是不是一定就是乘积最小的?
不一定。因为遍历顺序是按i从小到大进行的,当你找到第一组解时,j是固定的(j < i),但可能存在另外一组(i1, j1),其中i1 > i,但是j1更小,导致i1 * j1 < i * j。
举个例子,数组[1, 2, 3, 4, 5],target是6。遍历过程:
- i=2(值为3),查remain=3,不在表里,存入
- i=3(值为4),查remain=2,在表里(下标1),找到组合(3,1),乘积=3
- i=4(值为5),查remain=1,在表里(下标0),找到组合(4,0),乘积=0
看,第二次找到的组合乘积更小。所以如果你拿到第一组就返回,就错了。
正确做法是:遍历完整个数组,期间不断更新乘积最小的组合。不要提前break。
那怎么保证最终输出的下标顺序?题目一般会要求按下标从小到大输出,或者按原数组中的顺序输出,所以要记录min_i和min_j,最后统一比较大小再输出。
这里还有一个陷阱:有同学会想,那我能不能先把所有组合找出来,再算乘积?可以,但没必要。因为哈希表遍历过程中就可以维护最小值,时间复杂度仍然是O(n)。如果先收集所有组合再筛选,最坏情况下组合数量很多,反而退化。
2.4 下标从0还是从1开始
这是一个容易被忽略但会导致全盘皆输的细节。我见过好几个同学,代码逻辑完全正确,就是因为输出的时候没有加1,结果用例没过。
如果题目描述里说的是“房子编号从1开始”,那么输出的是下标+1;如果题目说的是“数组下标”,那就直接输出下标。怎么判断?看样例。题目给输入输出样例的时候,一定会体现这一点。
我的建议是:写代码时用一个变量offset来控制。如果题目要求从1开始,输出i+1和j+1;如果从0开始,直接输出i和j。这样调整起来只改一行。
3. Python实现:最简洁但也有细节
3.1 Python版本完整代码
import sys def solve(): data = sys.stdin.read().strip().split() idx = 0 results = [] while idx < len(data): n = int(data[idx]) idx += 1 arr = list(map(int, data[idx:idx+n])) idx += n target = int(data[idx]) idx += 1 visited = {} min_i, min_j = -1, -1 min_product = float('inf') for i, val in enumerate(arr): remain = target - val if remain in visited: j = visited[remain] product = i * j if product < min_product: min_product = product min_i, min_j = i, j visited[val] = i # 按题目要求输出,这里以下标从0开始为例 results.append(f"{min_i} {min_j}" if min_i != -1 else "-1") sys.stdout.write("\n".join(results)) if __name__ == "__main__": solve()3.2 为什么用sys.stdin.read()而不是input()
OD机试的输入数据量可能很大,用input()逐行读在极端情况下会慢。虽然这道题一般不卡这个,但养成用sys.stdin.read()批量读的习惯是好的,尤其是你要应对多组输入时,sys.stdin.read()一次性读取再拆分,代码更简洁,也不容易因为行数问题出错。
我见过有人这样写:
while True: try: n = int(input()) arr = list(map(int, input().split())) target = int(input()) except EOFError: break这个写法没有问题,但如果某个测试用例的数组跨行了(比如第二行太长被截断),就麻烦了。用sys.stdin.read().split()则完全无视换行和空格的区别,稳得多。
3.3 Python实现中的坑
第一个坑:字典的key如果重复,后出现的下标会覆盖前面的。比如数组[3, 3],target是6。遍历第一个3时,visited里没有3,存进去{3:0}。遍历第二个3时,remain=3,查到了下标0,正确。但如果数组是[3, 5, 3],target是6,遍历到第三个3时,visited[3]已经是0(因为第一次存了0,第二次是5不影响),结果是(2,0),正确。但要注意,visited[3]在第一次遇到3时就已经存了0,后面不会再更新为2,因为代码里visited[val] = i会在每次遍历时都执行,也就是说当遍历到下标2时,visited[3]被更新为2了。
等等,这里有个bug!我在上面的代码里,每次循环都会执行visited[val] = i,即使当前已经找到了匹配。这会导致什么?继续用[3, 5, 3]这个例子:
- i=0,val=3,remain=3,visited里没有3,visited[3]=0
- i=1,val=5,remain=1,visited里没有1,visited[5]=1
- i=2,val=3,remain=3,visited里有3!j=0,记录组合(2,0),然后执行visited[3]=2
这样看结果是对的。但换一种情况,如果数组是[3, 3, 5],target是6:
- i=0,val=3,remain=3,visited里没有3,visited[3]=0
- i=1,val=3,remain=3,visited里有3!j=0,记录组合(1,0),然后执行visited[3]=1
- i=2,val=5,remain=1,没有匹配
结果是(1,0),正确。但问题来了:如果以后再有需要用到visited[3]的地方,它已经是1了,不是0。不过这对本题没有影响,因为我们已经记录过(1,0)了,就算后面又找到一组(2,0),乘积是0 < 0?不对,乘积(2,0)=0和(1,0)=0相等,那也不能更新。
这里的关键问题是:更新visited会不会导致我们错过更好的解?答案是会。举个例子,数组[2, 3, 4, 2],target=4:
- i=0,val=2,remain=2,visited没有2,visited[2]=0
- i=1,val=3,remain=1,没有,visited[3]=1
- i=2,val=4,remain=0,没有,visited[4]=2
- i=3,val=2,remain=2,visited里有2(下标0),组合(3,0)
看起来没问题。但如果数组是[5, 1, 2, 3],target=7:
- i=0,val=5,remain=2,没有,visited[5]=0
- i=1,val=1,remain=6,没有,visited[1]=1
- i=2,val=2,remain=5,有!j=0,组合(2,0),visited[2]=2
- i=3,val=3,remain=4,没有
结果(2,0)。如果后来visited[5]被覆盖了(比如出现另一个5),那可能导致后面应该匹配5的漏掉。但这个场景里,我们已经在i=2时匹配了visited[5]=0,所以没问题。
真正的问题是你有没有必要在找到匹配后还执行visited[val] = i。其实可以加一行判断:只有没找到匹配时才更新。但更严谨的做法是:不管找没找到匹配,都更新visited,但更新的是当前val的下标,这不会影响已经记录的最优解,因为最优解的下标组合已经被记录下来了。
这样说可能有点绕。我举一个反例说明覆盖会导致问题:
数组[4, 1, 3, 2],target=5。
- i=0,val=4,remain=1,没有,visited[4]=0
- i=1,val=1,remain=4,有!j=0,组合(1,0),visited[1]=1
- i=2,val=3,remain=2,没有,visited[3]=2
- i=3,val=2,remain=3,有!j=2,组合(3,2),乘积=6 > 0,不更新
一切正常。但如果在visited[val] = i时更新的是已经匹配过的key呢?比如[2, 3, 2, 1],target=4:
- i=0,val=2,remain=2,没有,visited[2]=0
- i=1,val=3,remain=1,没有,visited[3]=1
- i=2,val=2,remain=2,有!visited[2]=0,组合(2,0),然后visited[2]=2
- i=3,val=1,remain=3,有!visited[3]=1,组合(3,1),乘积=3 > 0,不更新
结果正确。如果“覆盖后导致漏匹配”的场景,必须要求在覆盖之后,后面又出现了一个数和这个key匹配。但问题是,一旦visited[key]被更新,说明当前这个新值在原数组中更靠后,如果后续有一个数和它匹配,那么匹配的下标组合一定比之前更“靠后”,在同为匹配的情况下,下标更靠后的组合乘积不一定更大,但也可能更小。
举个例子,数组[1, 5, 2, 4],target=6。
- i=0,val=1,remain=5,没有,visited[1]=0
- i=1,val=5,remain=1,有!j=0,组合(1,0),visited[5]=1
- i=2,val=2,remain=4,没有,visited[2]=2
- i=3,val=4,remain=2,有!j=2,组合(3,2),乘积=6 > 0,不更新
正确。再看[1, 2, 2, 4],target=3:
- i=0,val=1,remain=2,没有,visited[1]=0
- i=1,val=2,remain=1,有!j=0,组合(1,0),visited[2]=1
- i=2,val=2,remain=1,有!j=0,组合(2,0),乘积=0,和(1,0)相等,不更新(因为product < min_product是严格小于,等于时不更新)。visited[2]=2
- i=3,val=4,remain=-1,没有
结果还是(1,0)。如果题目要求“如果乘积相同,输出下标更小的组合”,那这题答案是(1,0),我们的代码也输出(1,0),正确。但如果你写成了product <= min_product来更新,就会更新为(2,0),可能就错了。所以比较时用严格小于。
这是Python实现里最容易踩的坑之一,我专门写这么长就是想提醒你,看似简单的逻辑,边界情况真的很多。
3.4 Python版本的简化思路
其实还有一个更Pythonic的写法,用enumerate配合dict,代码更短:
visited = {} best = None for i, v in enumerate(arr): if target - v in visited: cand = (i, visited[target - v]) if best is None or cand[0] * cand[1] < best[0] * best[1]: best = cand visited[v] = i这个逻辑和我上面完整版一致,只是把组合存成元组。注意赋值顺序:先查后存,别搞反。
4. Java实现:HashMap与Integer的坑
4.1 Java版本完整代码
import java.util.*; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); while (sc.hasNext()) { int n = sc.nextInt(); int[] arr = new int[n]; for (int i = 0; i < n; i++) { arr[i] = sc.nextInt(); } int target = sc.nextInt(); Map<Integer, Integer> visited = new HashMap<>(); int bestI = -1, bestJ = -1; long bestProduct = Long.MAX_VALUE; for (int i = 0; i < n; i++) { int remain = target - arr[i]; if (visited.containsKey(remain)) { int j = visited.get(remain); long product = (long)i * j; if (product < bestProduct) { bestProduct = product; bestI = i; bestJ = j; } } visited.put(arr[i], i); } if (bestI == -1) { System.out.println(-1); } else { // 按题目要求调整下标输出 System.out.println(bestI + " " + bestJ); } } } }4.2 HashMap的containsKey与get
Java里实现哈希表首选HashMap。需要注意的几点:
第一,containsKey和get的配合。有些同学会写成:
Integer j = visited.get(remain); if (j != null) { ... }这样也行,因为HashMap的get如果key不存在会返回null。但如果你存的值本身就是null(这里不会),或者key对应的value是null(这里也不会),就可能出问题。用containsKey更安全直观。
第二,HashMap的泛型要写清楚。Map<Integer, Integer>表示key和value都是Integer。这里有个自动装箱的问题:visited.put(arr[i], i)会把int自动装箱成Integer,visited.get(remain)返回的是Integer,赋值给int j时会自动拆箱。这些操作在数据量小时没问题,但如果循环次数很大,装箱拆箱的损耗会累积。不过对于这道题,n一般在10^5以内,完全不用担心。
4.3 Java的“Integer缓存”问题
这里有一个很多人不知道的坑。Integer在-128到127之间的值是缓存的,也就是说:
Integer a = 100; Integer b = 100; System.out.println(a == b); // true,因为走缓存但:
Integer a = 200; Integer b = 200; System.out.println(a == b); // false,因为不在缓存范围,创建了两个对象这和我们的代码有什么关系?如果你用==来比较两个Integer的value,在缓存范围内没事,超出范围就出BUG。在跳房子这个题目里,数组元素可能很大,如果用:
if (visited.get(remain) == i) { ... }这比较的是Integer对象的引用,不是值!正确写法是:
if (visited.get(remain).equals(i)) { ... }或者直接用int承接后再比较:
int j = visited.get(remain); if (j == i) { ... }这个话题在Java面试八股文里也经常出现,如果你准备OD机试的同时也在准备Java面试,正好一并记牢。
4.4 Scanner的性能与替代方案
我上面的代码用了Scanner,简单易用,但性能一般。如果输入数据量非常大,Scanner的nextInt方法会比较慢,可能成为瓶颈。OD机试的Java环境一般不会卡这一点,但为了稳妥,你可以用更快的输入方式——BufferedReader加StringTokenizer:
BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer st = new StringTokenizer(br.readLine()); int n = Integer.parseInt(st.nextToken());这种方式比Scanner快一倍以上。我之前参加过一些要求严格的OJ比赛,用Scanner被TLE过,换成BufferedReader就过了。OD机试虽然不一定会卡得这么极限,但比赛嘛,多一分准备多一分胜算。
4.5 Java版本的下标输出细节
Java代码里输出bestI + " " + bestJ时,注意如果题目要求从1开始,要输出(bestI + 1) + " " + (bestJ + 1)。用括号括起来,不然字符串拼接会出错:
// 错误示例 System.out.println(bestI + 1 + " " + bestJ + 1); // 这会把bestI和1先做加法,然后拼字符串,但bestJ + 1会变成字符串拼接,输出 "3 51" 这种奇怪的东西这个错误非常经典,几乎每个Java新手都踩过。正确写法是:
System.out.println((bestI + 1) + " " + (bestJ + 1));另外,bestProduct我用了long类型,因为i * j在极端情况下(n很大,两边都是接近int上限的值)会溢出int。虽然实际题目n不会到那么大,但用long不亏,也体现你的严谨。
5. C++实现:unordered_map、迭代器与性能
5.1 C++版本完整代码
#include <iostream> #include <vector> #include <unordered_map> #include <climits> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; while (cin >> n) { vector<int> arr(n); for (int i = 0; i < n; i++) { cin >> arr[i]; } int target; cin >> target; unordered_map<int, int> visited; long long bestProduct = LLONG_MAX; int bestI = -1, bestJ = -1; for (int i = 0; i < n; i++) { int remain = target - arr[i]; auto it = visited.find(remain); if (it != visited.end()) { int j = it->second; long long product = (long long)i * j; if (product < bestProduct) { bestProduct = product; bestI = i; bestJ = j; } } visited[arr[i]] = i; } if (bestI == -1) { cout << -1 << endl; } else { cout << bestI << " " << bestJ << endl; } } return 0; }5.2 为什么要加ios::sync_with_stdio(false)
这一行是C++竞赛选手的“起手式”。cin和cout默认会和C标准库的stdio同步,以保证混用printf/cout时不会乱序,但代价是性能下降。关闭同步后,cin/cout的输入输出速度会大幅提升,接近printf/scanf的水平。
配合cin.tie(nullptr)可以解除cin和cout的绑定,避免每次输入前都刷新输出缓冲区。
这两个配置对这道题的效果可能不明显,但遇到大数据量题目,可能就是AC和TLE的区别。建议直接养成习惯。
5.3 unordered_map的find与insert
我用find而不是[]运算符来查找。为什么不直接用visited[remain]?
两个原因。第一,visited[remain]在key不存在时会插入一个默认值(int是0),这会污染哈希表,导致后续判断出错。比如remain=5,数组里没有5,但你visited[5]后,哈希表里就多了一个键值对(5,0),以后真的出现5时,你无法区分它是原本存在的还是刚刚插入的。
第二,visited[remain]在key不存在时返回0,如果在循环中间使用,可能让你误以为找到了匹配,实际上没有。
正确做法是:
auto it = visited.find(remain); if (it != visited.end()) { int j = it->second; ... }find返回的是迭代器,如果没找到,返回end()。找到后,it->first是key,it->second是value。
5.4 C++的迭代器失效问题
如果你在遍历unordered_map的同时修改它,要小心迭代器失效。但在跳房子这道题里,我们是在遍历数组,每轮循环里只对map做一次插入,不涉及遍历map本身,所以不存在迭代器失效问题。
不过,如果你在面试时被追问“unordered_map的rehash会怎样”,你要知道:当元素数量超过桶的数量时,unordered_map会触发rehash,所有迭代器失效。这就是为什么不能在遍历map时插入新元素。如果确实需要边遍历边插入,应该先记录下来,遍历完再插入。
5.5 C++版本的输出与int溢出
C++代码里我用了long long product = (long long)i * j;,强制把i转成long long再做乘法,防止int溢出。如果你直接写int product = i * j,当i和j都很大时,乘积会溢出,变成负数,导致product < bestProduct永远不成立,或者更糟,把错误的组合当成最优解。
这是C++初学者最容易犯的错误之一。Java里因为有long类型,这个问题好一些;C++里int和long在多数平台都是32位,务必注意。
5.6 C++的哈希表选型:map还是unordered_map
这也是个高频考点。map底层是红黑树,有序但插入和查找都是O(log n);unordered_map底层是哈希表,平均O(1),但无序,最坏O(n)。
这道题不需要有序性,所以用unordered_map更合适。但要注意,unordered_map的常数比map大,在数据量小时(比如n小于100),用map可能反而更快,因为哈希函数的计算也有开销。
不过OD机试的数据范围通常比较大,所以还是推荐unordered_map。如果你担心最坏情况的O(n),可以换map,但没必要,因为题目不会故意构造哈希冲突的数据来卡你(至少华为OD的题目目前没有这么恶意)。
6. 三种语言对比与选型建议
6.1 性能对比
同一道题,三种语言的运行速度和代码量有明显差异。我整理了一个表格:
| 语言 | 时间复杂度 | 空间复杂度 | 代码量 | 适用场景 |
|---|---|---|---|---|
| Python | O(n) | O(n) | 最少,约15行 | 快速实现,笔试首选 |
| Java | O(n) | O(n) | 中等,约30行 | 企业主流,机试常用 |
| C++ | O(n) | O(n) | 中等,约30行 | 性能最高,竞赛常用 |
Python代码最短,但运行最慢;Java和C++代码相当,但C++运行最快。OD机试支持这三种语言,选哪种完全看你熟悉程度。
我的建议是:你熟悉哪门就用哪门,不要临时换语言。机试考的是算法思维,不是语言PK。如果你Python写得很溜,就用Python;如果C++顺手,就用C++。
但有一点要注意:如果你选了Python,且题目明确要求用“ACM模式”,你必须自己处理输入输出。很多Python选手在力扣上习惯了函数签名,到了OD机试连while读多组输入都写不利索,这就很吃亏。我见过太多人挂在输入输出上,算法本身倒是对的。
6.2 我在实际刷题中的体会
我自己刷题习惯用C++,因为性能上限高,而且STL的unordered_map、vector用起来非常顺手。但我也用Python做过这道题,发现Python的字典语法简洁到“令人发指”,写起来确实快。
在OD机试中,我的策略是:先用Python快速确认思路,再用C++写AC代码。这不是说Python不行,而是C++的编译型语言在极限情况下更稳。如果你只熟悉Java,那就Java一把梭,完全没问题。
还有个特别实用的建议:提前写好输入输出模板。不要到考场上再默写Scanner用法或cin的配置,把这些固定代码存在本地记事本里,考试时直接复制粘贴改改逻辑就行。我参加机试时,光输入输出模板就省了我10分钟。
7. 常见问题与调试技巧实录
7.1 问题:一直WA,但本地测试都通过
这是最让人抓狂的情况。可能性有很多,我按概率从高到低列一下:
第一,输出格式不对。多了一个空格、少了一个换行、大小写不一致、逗号换成空格或空格换成逗号,都会被判WA。华为OD机试对输出格式要求非常严格,你必须在最后输出一行,末尾有没有多余空格都不行。
第二,下标从0还是从1。这是“跳房子I”最容易翻车的地方。你再仔细读一遍题目,看“房子编号”还是“数组下标”。很多题目描述会说“第1个房子”“第2个房子”,那就是从1开始。
第三,多组输入处理不对。如果你用input()一行一行读,而题目实际是多组输入,你可能只处理了第一组,后面的组被丢掉了。所以我才推荐用sys.stdin.read()或cin >> n配合while循环。
7.2 问题:同样的代码,Java比Python快很多
这是正常现象,不必担心。Java和C++是编译型,Python是解释型,运行速度天然有差距。只要你的算法复杂度是O(n),n在10^5以内,Python在OD机试的1秒时限内也够用。
但如果n到了10^6,Python的O(n)可能会卡在0.5秒左右,而C++几乎是瞬间。遇到这种大数据量,建议用C++。平时刷题可以用Python练思路,考试用C++求稳。
7.3 问题:哈希表里的key重复,到底取哪个下标
前面我详细分析过,visited[val] = i每次都会更新,所以同一个value出现多次时,哈希表里存的是最后出现的下标。
那这对找最优解有影响吗?我们分析过,匹配时用的是较早出现的下标(因为j从visited里取出来的,一定是当前value之前出现的最后一个),而当前i是较晚的。如果你想要“下标乘积最小”,那么同一个value取更早的下标,乘积更小,但我们的代码里因为每次覆盖,所以拿到的是“当前value最后一次出现的位置”。
等一下,这会不会错过更小的乘积?我们来构造一个例子:
数组[1, 4, 3, 1, 4],target=5。
- i=0,val=1,remain=4,没有,visited[1]=0
- i=1,val=4,remain=1,有!j=0,组合(1,0),visited[4]=1
- i=2,val=3,remain=2,没有,visited[3]=2
- i=3,val=1,remain=4,有!visited[4]=1,组合(3,1),乘积=3 > 0,不更新,visited[1]=3
- i=4,val=4,remain=1,有!visited[1]=3,组合(4,3),乘积=12 > 0,不更新
最终结果是(1,0),正确。但如果数组是[2, 3, 4, 2, 2],target=4:
- i=0,val=2,remain=2,没有,visited[2]=0
- i=1,val=3,remain=1,没有,visited[3]=1
- i=2,val=4,remain=0,没有,visited[4]=2
- i=3,val=2,remain=2,有!j=0,组合(3,0),visited[2]=3
- i=4,val=2,remain=2,有!j=3,组合(4,3),乘积=12 > 0,不更新
结果(3,0)。但如果数组是[2, 3, 2, 4],target=4:
- i=0,val=2,remain=2,没有,visited[2]=0
- i=1,val=3,remain=1,没有,visited[3]=1
- i=2,val=2,remain=2,有!j=0,组合(2,0),visited[2]=2
- i=3,val=4,remain=0,没有
结果(2,0)。如果后面还有一个2,比如[2, 3, 2, 4, 2]:
- i=0,val=2,visited[2]=0
- i=1,val=3,visited[3]=1
- i=2,val=2,remain=2,有!j=0,组合(2,0),visited[2]=2
- i=3,val=4,没有
- i=4,val=2,remain=2,有!j=2,组合(4,2),乘积=8 > 0,不更新
结果(2,0)。看起来覆盖不会影响最优解,因为更靠后的匹配,其乘积往往不会比更靠前的匹配小。严格证明是:如果存在两对匹配(i1, j1)和(i2, j2),其中i1 < i2且j1 < j2,那么i1*j1和i2*j2的大小不确定;但如果j1是同一个value的多次出现,哈希表里存的只会是最后一次,所以你能匹配到的是“当前value最后一次出现的位置”,这可能是j2而不是j1,导致漏掉i1*j1这个更小的组合。
我试着构造一下:数组[1, 2, 1, 2],target=3。
- i=0,val=1,remain=2,没有,visited[1]=0
- i=1,val=2,remain=1,有!j=0,组合(1,0),visited[2]=1
- i=2,val=1,remain=2,有!visited[2]=1,组合(2,1),乘积=2 > 0,不更新,visited[1]=2
- i=3,val=2,remain=1,有!visited[1]=2,组合(3,2),乘积=6 > 0,不更新
结果(1,0)。这里没有漏掉更小的。如果一开始的组合乘积更大呢?构造[9, 1, 8, 9, 2],target=10:
- i=0,val=9,remain=1,没有,visited[9]=0
- i=1,val=1,remain=9,有!j=0,组合(1,0),visited[1]=1
- i=2,val=8,remain=2,没有,visited[8]=2
- i=3,val=9,remain=1,有!visited[1]=1,组合(3,1),乘积=3 > 0,不更新,visited[9]=3
- i=4,val=2,remain=8,有!visited[8]=2,组合(4,2),乘积=8 > 0,不更新
结果(1,0)。如果出现的顺序是反的:[1, 9, 2, 9, 8],target=10:
- i=0,val=1,remain=9,没有,visited[1]=0
- i=1,val=9,remain=1,有!j=0,组合(1,0),visited[9]=1
- i=2,val=2,remain=8,没有,visited[2]=2
- i=3,val=9,remain=1,有!j=0,组合(3,0),乘积=0,不更新(因为等于0 < 0为假),visited[9]=3
- i=4,val=8,remain=2,有!visited[2]=2,组合(4,2),乘积=8 > 0,不更新
结果还是(1,0),乘积0最小。看起来覆盖没啥影响?其实覆盖确实不太可能漏掉更小乘积,因为你要漏掉的是“最早的匹配”,而当你遇到一个重复value时,它对应的匹配组合,i比之前更大,j也可能因为覆盖而变大,乘积可能变大而不是变小。所以不用太担心覆盖问题。但稳妥起见,可以只在没有匹配时才更新visited,这样不会被覆盖逻辑干扰。
7.4 问题:如何确认自己是“跳房子I”还是“跳房子II”
“跳房子II”在OD题库里的意思是:数组改为二维的,或者要求输出所有组合而不是一组。如果你在考试时发现题目的输入不止一个数组,比如每行两个数,那很可能是II。万变不离其宗,核心还是两数之和的变体,只是存储结构和筛选条件变了。
遇到这种情况,我的建议是:先读样例,把样例在纸上模拟一遍,确认规则后再写代码。不要急着套模板,同名题不同描述的坑我已经说了很多次。
8. 机试准备与考试技巧
8.1 刷题策略
如果你想系统准备华为OD机试,不要只刷“跳房子I”这一道,而是把它归到“哈希表”这个专题里,配套刷:两数之和、三数之和、最长连续序列、字母异位词分组。这些题的解题套路都是“空间换时间”,用哈希表加速查找。
我的刷题计划是这样的:每道题先自己思考10分钟,想不出来就看题解,看懂后合上书自己写一遍,然后对比最优解,总结套路。不要直接抄,也不要只看不写。刷题最忌讳的就是“眼睛会了,手不会”。我见过太多同学,收藏了100道题解,考试时还是AC不了,就是因为动手太少。
8.2 解题模板的积累
针对“两数之和”这一类题,可以总结一个通用模板:
def two_sum(nums, target): visited = {} for i, num in enumerate(nums): if target - num in visited: return [visited[target - num], i] visited[num] = i return []这个模板在所有“找两数之和”的变体里都能用。考试时先把这个框架写出来,再根据题目要求调整输出格式和筛选条件,效率会高很多。
8.3 考试现场的时间分配
OD机试一般有2到3道题,总时长约150分钟。“跳房子I”这种难度,建议控制在20分钟以内。包括读题、构思、编码、测试样例。
如果你20分钟还卡在WA上,不要死磕,先跳到下一题。把能拿的分都拿了,最后有时间再回来看。考试最怕的是时间分配失衡,前面一道题消耗太久,后面简单题没时间做,直接崩盘。
8.4 考前模拟
考前至少做一次模拟考试:限时150分钟,用牛客网或华为OD模拟系统,一次做完三道题。通过模拟可以暴露两个问题:一是输入输出处理不熟练,二是时间分配不合理。我是在模拟时才发现自己写多组输入太慢,专门练了几次sys.stdin.read()的写法才改过来的。
另外,你说的“双机位C卷”,OD机试现在确实有严格的防作弊机制,两个摄像头监控。这意味着你不能依赖手机搜索,平时刷题就要养成不查资料写代码的习惯。不要小看这一点,很多人一到“不能搜索”的环境,思路就断片,代码也写不利索。平时练习时尽量不看题解独立写,考试就会轻松很多。
9. 个人经验总结与最后的提醒
“跳房子I”这道题,如果满分是100分,读懂题意占30分,哈希表思路占30分,输入输出处理占30分,边界条件占10分。很多人挂在输入输出上,也有人挂在“下标乘积最小”的筛选上,还有人挂在“从0还是从1开始”上。
我个人的经验是:拿到题先别急着写,花3分钟把输入、输出、样例过一遍,搞清楚三件事——数据范围多大、输入是一个还是多个、输出从0还是从1。然后再动手。这3分钟不会浪费,反而能避免掉进坑里。
说到“跳房子”这个游戏本身,很像我们小时候在地面上画的格子,一格一格往前跳。但到了机试里,它变成了哈希表的经典应用。如果你真的掌握了这类题,后续遇到“三数之和”“四数之和”也都能迎刃而解,因为核心逻辑是相通的:用哈希表记录已经遍历过的信息,避免重复计算。
最后一句话送给正在准备OD机试的你:不要迷信“运气”,多刷一道题,考场就多一分底气。写代码的时候仔细点,提交之前检查一遍输出格式,你离上岸就差这“最后一公里”。