找到两个数组的前缀公共数组 图解题解
这道题到底在问什么
- 输入
- A=[1,3,2,4], B=[3,1,2,4]
- 输出
- [0,2,3,4]
- 输入
- A=[2,3,1], B=[3,1,2]
- 输出
- [0,1,3]
最优解:一步一步想明白
- 3记牢这一句:一边扫一边往两个已见集合里登记,谁两边都露过面,谁就是公共数,当前公共数的个数就是 C[i]。因为是排列,每个数最多出现一次,判断非常干净。下面从下标 0 开始扫。
- 4这是数组 A,紫色指针待会儿会从最左边一格一格往右挪。右边这张登记表现在是空的,它记录每个数字目前落在哪一侧:只在 A 出现记 A 侧,只在 B 出现记 B 侧,两边都出现就升级成 A·B 公共。C[i] 就是表里公共那类有几个。开局两个集合都空,公共个数是 0。
- 5指针挪到下标 0,读到 A 这一位是 2。把 2 记进 A 的已见集合,登记表里它这一行亮起来。此刻 2 还只在 A 露过面,先挂在 A 侧,等 B 那边也遇到它才算公共。
- 6同一步再看 B 的第 0 位,读到的是 4。把 4 记进 B 的已见集合。它现在还只在 B 侧,A 那边还没碰到过它,暂时不算公共,公共个数仍是 0。
- 7这一步结算。数一数登记表里标着 A·B 公共的有几个,就是 0 个,对应到数组 A 上就是 0 个绿格。所以 C[0] 填 0。从初始公共个数 0 来看,这一步还没抓到公共数。
- 8指针挪到下标 1,读到 A 这一位是 4。把 4 记进 A 的已见集合,登记表里它这一行亮起来。注意 4 之前已经在 B 侧出现过,这一记就把它升级成 A·B 公共了。
- 9同一步再看 B 的第 1 位,读到的是 2。把 2 记进 B 的已见集合。而 2 之前 A 侧已经登记过,两边都齐了,2 当场升级成公共,公共个数涨到 2。
- 10这一步结算。数一数登记表里标着 A·B 公共的有几个,就是 2 个,对应到数组 A 上就是 2 个绿格。所以 C[1] 填 2。比上一位多了 2 个,公共集合又长大了。
- 11指针挪到下标 2,读到 A 这一位是 1。把 1 记进 A 的已见集合,登记表里它这一行亮起来。此刻 1 还只在 A 露过面,先挂在 A 侧,等 B 那边也遇到它才算公共。
- 12同一步再看 B 的第 2 位,读到的是 5。把 5 记进 B 的已见集合。它现在还只在 B 侧,A 那边还没碰到过它,暂时不算公共,公共个数仍是 2。
- 13这一步结算。数一数登记表里标着 A·B 公共的有几个,就是 2 个,对应到数组 A 上就是 2 个绿格。所以 C[2] 填 2。和上一位持平,说明这一步没抓到新的公共数。
- 14指针挪到下标 3,读到 A 这一位是 5。把 5 记进 A 的已见集合,登记表里它这一行亮起来。注意 5 之前已经在 B 侧出现过,这一记就把它升级成 A·B 公共了。
- 15同一步再看 B 的第 3 位,读到的是 1。把 1 记进 B 的已见集合。而 1 之前 A 侧已经登记过,两边都齐了,1 当场升级成公共,公共个数涨到 4。
- 16这一步结算。数一数登记表里标着 A·B 公共的有几个,就是 4 个,对应到数组 A 上就是 4 个绿格。所以 C[3] 填 4。比上一位多了 2 个,公共集合又长大了。这一轮 A 补上了 B 侧的 5,B 又补上了 A 侧的 1,所以公共数一次涨了 2 个。
- 17指针挪到下标 4,读到 A 这一位是 3。把 3 记进 A 的已见集合,登记表里它这一行亮起来。此刻 3 还只在 A 露过面,先挂在 A 侧,等 B 那边也遇到它才算公共。
- 18同一步再看 B 的第 4 位,读到的是 6。把 6 记进 B 的已见集合。它现在还只在 B 侧,A 那边还没碰到过它,暂时不算公共,公共个数仍是 4。
- 19这一步结算。数一数登记表里标着 A·B 公共的有几个,就是 4 个,对应到数组 A 上就是 4 个绿格。所以 C[4] 填 4。和上一位持平,说明这一步没抓到新的公共数。
- 20指针挪到下标 5,读到 A 这一位是 6。把 6 记进 A 的已见集合,登记表里它这一行亮起来。注意 6 之前已经在 B 侧出现过,这一记就把它升级成 A·B 公共了。
- 21同一步再看 B 的第 5 位,读到的是 3。把 3 记进 B 的已见集合。而 3 之前 A 侧已经登记过,两边都齐了,3 当场升级成公共,公共个数涨到 6。
- 22这一步结算。数一数登记表里标着 A·B 公共的有几个,就是 6 个,对应到数组 A 上就是 6 个绿格。所以 C[5] 填 6。比上一位多了 2 个,公共集合又长大了。
- 23六位全部扫完。整条 A 都变绿了,因为到最后 A 和 B 见过的数字完全一样,六个数全是公共数。回看这张 C 等于 0、2、2、4、4、6,它自始至终不下降:每读一位,已见集合只增不减,公共数只会持平或变多,绝不会往回掉。这就是这道题最核心的规律。
⚠️ 容易写错的地方
✗ 错:把 C[i] 理解成 A[i] 和 B[i] 这一位相不相等
✓ 对:C[i] 是整个前缀里公共数字的累计个数
公共看的是前缀这一整段,不是当前这一位对不对齐,某数可能在 A、B 里位置不同却都出现过
✗ 错:公共数字重复计数导致 C 偏大
✓ 对:每个值只算一次,一旦成为公共就固定在集合里
因为是排列每个值只出现一次,升级成公共后无需再动,重复累加会算多
✗ 错:担心 C 会中途变小
✓ 对:C 一定不下降
已见集合只增不减,前缀越长交集只会持平或变大,不可能丢掉已经确认的公共数
✗ 错:计数数组开成长度 n 而越界
✓ 对:值域是 1 到 n,数组要开 n 加 1
下标要能放到 n,少开一格访问 cnt[n] 会越界
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def findThePrefixCommonArray(self, A: List[int], B: List[int]) -> List[int]:
ans = []
cnt1 = Counter()
cnt2 = Counter()
for a, b in zip(A, B):
cnt1[a] += 1
cnt2[b] += 1
t = sum(min(v, cnt2[x]) for x, v in cnt1.items())
ans.append(t)
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
vector<int> findThePrefixCommonArray(vector<int>& A, vector<int>& B) {
int n = A.size();
vector<int> ans(n);
vector<int> cnt1(n + 1), cnt2(n + 1);
for (int i = 0; i < n; ++i) {
++cnt1[A[i]];
++cnt2[B[i]];
for (int j = 1; j <= n; ++j) {
ans[i] += min(cnt1[j], cnt2[j]);
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int[] findThePrefixCommonArray(int[] A, int[] B) {
int n = A.length;
int[] ans = new int[n];
int[] cnt1 = new int[n + 1];
int[] cnt2 = new int[n + 1];
for (int i = 0; i < n; ++i) {
++cnt1[A[i]];
++cnt2[B[i]];
for (int j = 1; j <= n; ++j) {
ans[i] += Math.min(cnt1[j], cnt2[j]);
}
}
return ans;
}
}复杂度
时间
O(n²)
参考代码外层扫 n 位,每一位再从 1 到 n 把两个计数数组的较小值累加一遍,是一层套一层,共约 n 乘 n 次操作。用位掩码或哈希维护交集可优化到 O(n),但常数与思路更绕
空间
O(n)
两个长度 n 加 1 的计数数组(或两个 Counter),外加长度 n 的答案数组。都随 n 线性增长,不额外开二维结构
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 找到两个数组的前缀公共数组 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题最直接的思路是什么?+
一边从左往右扫,一边给 A、B 各维护一个已见集合。每到下标 i,把 A[i]、B[i] 分别记进两侧,当前两个集合交集的大小就是 C[i]。因为是排列每个数只出现一次,判断交集非常干净。
怎么把复杂度做到 O(n)?+
用位掩码。拿两个整数 pa、pb 分别当 A、B 的已见集合,读到值 v 就把对应的第 v 位置 1。C[i] 就是 pa 与 pb 按位与之后的二进制里 1 的个数。每步只做常数次位运算,数二进制里 1 的个数也是常数级,整体 O(n)。参考代码为了直观用的是计数数组,是 O(n²)。
为什么用计数数组也能正确统计?+
因为是排列,每个值在 A、B 里最多各出现一次,对应计数最多是 1。某个值只有两边都出现时,cnt1 和 cnt2 才都是 1,取较小值贡献 1;只要有一边没出现,较小值就是 0。把所有值的较小值加起来,正好等于同时出现的数字个数。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 找到两个数组的前缀公共数组 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。