最大兼容性评分和 图解题解
这道题到底在问什么
- 输入
- students=[[1,1,0],[1,0,1],[0,0,1]], mentors=[[1,0,0],[0,0,1],[1,1,0]]
- 输出
- 8
- 输入
- students=[[0,0],[0,0],[0,0]], mentors=[[1,1],[1,1],[1,1]]
- 输出
- 0
先想最直接的笨办法
记牢这两步:先把每一对的评分算成表 g,再回溯枚举所有一一配对取最大和。下面先把 g 一格一格填出来。(动画第 3 步)
最优解:为什么这么做
一句话答案:LeetCode 1947 最大兼容性评分和:先把每对学生导师答案相同的题数算成 m×m 的 g 表,再回溯枚举全部一一配对取最大和。m≤8 时 m! 才四万多,全枚举跑得动,时间 O(m²·n+m·m!)、空间 O(m²)(g 表)。
学生和导师一一配对,评分和最大在求什么
students 和 mentors 各有 m 行,每行是一人对 n 道是非题的作答,只有 0 和 1。每名学生恰好配一名导师、每名导师只被配一次,一对的兼容性评分等于两人答案相同的题数,求所有配对方案里评分和的最大值。示例 students=[[1,1,0],[1,0,1],[0,0,1]]、mentors=[[1,0,0],[0,0,1],[1,1,0]],答案是 8。
每人抢自己分最高的导师,为什么靠不住
一名学生抢走一名导师,后面所有人的选项都跟着变。本例按行贪心碰巧也凑到 8,可贪心(每步只挑眼前最高分)换一组答卷就没人担保。好在 m 最大 8,一一配对共 m! 种(第一人 m 种选、第二人剩 m−1 种…连乘)、8! 才 40320,全走一遍负担得起。真正的浪费在每种配对都现场逐题比答案:多乘一个 n,同一对人的分数被重比几千次。
为什么配对之前要先填一张 g 表
把比较和配对拆开:先建 m 行 m 列的兼容矩阵 g,g[i][j](行是学生、列是导师,都从 0 数起)存学生 i 和导师 j 答案相同的题数。填表对 m×m 个组合各比 n 道题,O(m²·n)(大 O 记号,粗略记操作量和数据规模的关系)一次算清。之后谁配谁值几分,查表就行。
回溯凭什么把每种配对不重不漏走一遍
回溯(一条路走到底,退回来撤销选择再换下一条)按学生顺序推进:轮到学生 i,在没被占用的导师里挑一个 j,把 vis[j] 标成占用、当前和加上 g[i][j],往下处理学生 i+1;走完退回来把 vis[j] 撤销,导师 j 才能进别的方案。i 走到 m 说明配齐,拿当前和刷新最大值。每层恰好定一名学生、vis 保证导师只被占一次,每条完整路径都是合法配对,m! 种各出现一次。
拿题面示例把 g 表和六种配对全算一遍
先填表。学生 0 答 1、1、0:和导师 0 的 1、0、0 有 2 题相同,和导师 1 的 0、0、1 全不同,和导师 2 的 1、1、0 全同,g 第 0 行是 2、0、3。同法算出学生 1 对三名导师是 2、2、1,学生 2 是 1、3、0。
再枚举六种配对,按学生 0、1、2 各配的导师记:[M0,M1,M2] 得 2+2+0=4,[M0,M2,M1] 得 2+1+3=6,[M1,M0,M2] 得 0+2+0=2,[M1,M2,M0] 得 0+1+1=2,[M2,M0,M1] 得 3+2+3=8,[M2,M1,M0] 得 3+2+1=6。最大 8,和题面输出对上。
vis 忘了撤销,答案为什么卡在 4 不动
第一条路 [M0,M1,M2] 走完得 4,回来不撤销 vis,三名导师全被永久占用,后面的分支全被跳过,答案卡在 4,比正确的 8 少一半。复杂度:建表 O(m²·n),枚举 m! 种配对、每种沿途约 m 次加法,合计 O(m²·n+m·m!);空间主要是 g 表的 O(m²)。第二组示例学生全答 0、导师全答 1,怎么配每对都 0 题相同,答案 0 就是合法输出。m 为 1 时只有一种配法,直接数那一对相同的题数,全同就拿满 n 分。
▶ 动画逐步走查(共 28 步)——想跟着动画一帧帧对照就展开
- 3记牢这两步:先把每一对的评分算成表 g,再回溯枚举所有一一配对取最大和。下面先把 g 一格一格填出来。
- 4每行一名学生这是三名学生的答卷,每一行是一名学生对三道题的作答,格子里是 0 或 1。学生 0 答的是 1、1、0,学生 1 答 1、0、1,学生 2 答 0、0、1。等会儿要拿它逐行去和导师比。
- 5每行一名导师这是三名导师的答卷。导师 0 答 1、0、0,导师 1 答 0、0、1,导师 2 答 1、1、0。兼容性评分就是把某个学生这一行和某个导师这一行,逐题对齐比一比,数出相同的题数。
- 6先把一张 3 行 3 列的空表 g 摆出来。行是学生、列是导师,第 i 行第 j 列这一格,填的就是学生 i 和导师 j 答案相同的题数。接下来一格一格填,填法就是把对应的两行答案逐题比对、数相同的个数。
- 7算学生 0 和导师 0。学生 0 答 1、1、0,导师 0 答 1、0、0,三道题逐个对:第1题 相同、第2题 不同、第3题 相同,相同的有 2 道,所以 g[0][0] 填 2。
- 8算学生 0 和导师 1。学生 0 答 1、1、0,导师 1 答 0、0、1,三道题逐个对:第1题 不同、第2题 不同、第3题 不同,相同的有 0 道,所以 g[0][1] 填 0。
- 9算学生 0 和导师 2。学生 0 答 1、1、0,导师 2 答 1、1、0,三道题逐个对:第1题 相同、第2题 相同、第3题 相同,相同的有 3 道,所以 g[0][2] 填 3。
- 10算学生 1 和导师 0。学生 1 答 1、0、1,导师 0 答 1、0、0,三道题逐个对:第1题 相同、第2题 相同、第3题 不同,相同的有 2 道,所以 g[1][0] 填 2。
- 11算学生 1 和导师 1。学生 1 答 1、0、1,导师 1 答 0、0、1,三道题逐个对:第1题 不同、第2题 相同、第3题 相同,相同的有 2 道,所以 g[1][1] 填 2。
- 12算学生 1 和导师 2。学生 1 答 1、0、1,导师 2 答 1、1、0,三道题逐个对:第1题 相同、第2题 不同、第3题 不同,相同的有 1 道,所以 g[1][2] 填 1。
- 13算学生 2 和导师 0。学生 2 答 0、0、1,导师 0 答 1、0、0,三道题逐个对:第1题 不同、第2题 相同、第3题 不同,相同的有 1 道,所以 g[2][0] 填 1。
- 14算学生 2 和导师 1。学生 2 答 0、0、1,导师 1 答 0、0、1,三道题逐个对:第1题 相同、第2题 相同、第3题 相同,相同的有 3 道,所以 g[2][1] 填 3。
- 15算学生 2 和导师 2。学生 2 答 0、0、1,导师 2 答 1、1、0,三道题逐个对:第1题 不同、第2题 不同、第3题 不同,相同的有 0 道,所以 g[2][2] 填 0。
- 16九格全部填好,兼容矩阵 g 出炉。往后配对时想知道某个学生配某个导师值几分,直接查这张表就行,不用再逐题比。这些分数有高有低,配对要看整体搭法,不能只贪当前一格。下面进入第二步:枚举所有一一配对,找评分和最大的那种。
- 17开枚举 · 给学生 0、1、2 各配一个空闲导师
- 18学生 0 配 导师 0 · 得 g[0][0] = 2
- 19学生 1 配 导师 1 · 累计 2 + g[1][1] = 4
- 20学生 2 只剩 导师 2 · 方案 [M0,M1,M2] 评分和 = 4
- 21回退学生 1 · 改配 导师 2 · 累计 2 + g[1][2] = 3
- 22学生 2 配 导师 1 · 方案 [M0,M2,M1] 评分和 = 6
- 23回到学生 0 · 改配 导师 1 · 起手只有 g[0][1] = 0
- 24这一大类两种方案 · [M1,M0,M2] 与 [M1,M2,M0] 都只有 2
- 25回到学生 0 · 改配 导师 2 · 起手 g[0][2] = 3
- 26学生 1 配 导师 0 · 累计 3 + g[1][0] = 5
- 27学生 2 配 导师 1 · 方案 [M2,M0,M1] 评分和 = 8 · 最大值刷新
- 28回退学生 1 · 改配 导师 1 · 累计 3 + g[1][1] = 5
- 29学生 2 配 导师 0 · 方案 [M2,M1,M0] 评分和 = 6
- 30六种一一配对全枚举完 · 最大评分和 = 8
⚠️ 容易写错的地方
✗ 错:让每个学生各自贪心挑当前评分最高的空闲导师
✓ 对:回溯枚举所有一一配对,取评分和最大的一种
局部最优不等于全局最大,一个学生抢走某导师会连累后面的人,贪心在有些数据上会错过最优解
✗ 错:递归返回后忘了把 vis[j] 置回 false
✓ 对:回来立刻撤销占用标记
不撤销,这个导师会被后续分支当成永久占用,漏掉一大批本可行的配对方案
✗ 错:在 dfs 里每次都重新逐题数相同个数
✓ 对:开局先把兼容矩阵 g 预处理好,配对时直接查表
评分会被反复重算 m 的阶乘次,预处理一次存下来能省掉大量重复比较
✗ 错:把学生和导师当成可以多对一
✓ 对:学生导师是一一配对,每个导师只配一名学生
用 vis 保证每个导师只被占用一次,才符合双射的题意
完整代码(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 TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def maxCompatibilitySum(
self, students: List[List[int]], mentors: List[List[int]]
) -> int:
def dfs(i: int, s: int):
if i >= m:
nonlocal ans
ans = max(ans, s)
return
for j in range(m):
if not vis[j]:
vis[j] = True
dfs(i + 1, s + g[i][j])
vis[j] = False
ans = 0
m = len(students)
vis = [False] * m
g = [[0] * m for _ in range(m)]
for i, x in enumerate(students):
for j, y in enumerate(mentors):
g[i][j] = sum(a == b for a, b in zip(x, y))
dfs(0, 0)
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 <optional>
#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;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
int maxCompatibilitySum(vector<vector<int>>& students, vector<vector<int>>& mentors) {
int m = students.size();
int n = students[0].size();
vector<vector<int>> g(m, vector<int>(m));
vector<bool> vis(m);
for (int i = 0; i < m; ++i) {
for (int j = 0; j < m; ++j) {
for (int k = 0; k < n; ++k) {
g[i][j] += students[i][k] == mentors[j][k];
}
}
}
int ans = 0;
function<void(int, int)> dfs = [&]( int i, int s ) {
if (i >= m) {
ans = max(ans, s);
return;
}
for (int j = 0; j < m; ++j) {
if (!vis[j]) {
vis[j] = true;
dfs(i + 1, s + g[i][j]);
vis[j] = false;
}
}
};
dfs(0, 0);
return ans;
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
private int m;
private int ans;
private int[][] g;
private boolean[] vis;
public int maxCompatibilitySum(int[][] students, int[][] mentors) {
m = students.length;
g = new int[m][m];
vis = new boolean[m];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < m; ++j) {
for (int k = 0; k < students[i].length; ++k) {
if (students[i][k] == mentors[j][k]) {
++g[i][j];
}
}
}
}
dfs(0, 0);
return ans;
}
private void dfs(int i, int s) {
if (i >= m) {
ans = Math.max(ans, s);
return;
}
for (int j = 0; j < m; ++j) {
if (!vis[j]) {
vis[j] = true;
dfs(i + 1, s + g[i][j]);
vis[j] = false;
}
}
}
}复杂度
时间
O(m·m·n + m·m!)
建兼容矩阵 g 要对 m 乘 m 个学生导师对、各比 n 道题,是 m 乘 m 乘 n;回溯枚举 m 的阶乘种一一配对,每种沿途累加约 m 步,是 m 乘 m 阶乘。m 最大 8 时阶乘四万多,整体几十万量级,能过
空间
O(m·m)
按峰值算。兼容矩阵 g 占 m 乘 m;vis 标记数组和递归栈深度都是 m 量级。峰值主要是那张 g 表,O(m 乘 m)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最大兼容性评分和 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
m 到 8 都能全枚举,什么时候必须换状压 DP?+
m! 涨得比 2^m 快得多:m=8 时 40320 还能跑,m=15 时 m! 已是万亿量级。这时换状压 DP——动态规划指把「前若干名学生配完、哪些导师被占」这种局面的最优分存下来避免重算;状压指把「哪些导师已被配走」压成一个二进制数 mask,第 j 位是 1 表示导师 j 已被占,这串二进制标记就是位掩码,LeetCode 2002 双回文子序列就是同款位掩码枚举。dp[mask] 记被占导师集合为 mask 时,前 k 名学生(k 等于 mask 里 1 的个数)能拿的最大总分;更新时给学生 k 逐个试 mask 里还是 0 的导师 j,用 dp[mask]+g[k][j] 刷新 dp[mask|1<
不先建 g 表,回溯里现场比答案行不行?+
行,答案一样,只是慢:同一对学生导师在不同配对方案里反复出现,现比就要把 n 道题反复重数,时间多乘一个 n。先花 O(m²·n) 把分数存进 g 表,之后每次只做一次查表加法,是拿 O(m²) 的空间换掉重复比较。m 不超过 8 时两种写法都能过,但先建表是这类配对题的通用起手。
这题和二分图最大权匹配是什么关系?+
学生是一侧点、导师是另一侧点、g[i][j] 是连边的权,一一配对求评分和最大,正是二分图(点分成两边、边只跨两边连)上的最大权完美匹配。KM 算法能在 O(m³) 内解决大规模的这类匹配,但实现长、细节多;本题 m≤8,回溯或状压 DP 几行就写完,面试里除非追问大规模,不必上 KM。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最大兼容性评分和 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。