二分图是图论里结构最干净的一类:点能分成左右两堆,每条边都从左边连到右边,堆内没有边。结构干净,问题就好算——“把 n 个任务分给 m 个人,每人最多一个、每人只会其中几项”,这类分配问题一旦建成二分图,就变成了求最大匹配。
| 要做的事 | 做法 | 复杂度 | 本节位置 |
|---|---|---|---|
| 判断一个图是不是二分图 | BFS 染色,遇同色相邻就否定 | O(n + m) | 本节第一部分 |
| 二分图最大匹配 | 匈牙利算法,逐个左侧点找增广路 | O(n·E),n = 500、E = 5×104 时 2.5×107 | 本节第二部分 |
| 最小点覆盖(二分图) | König 定理:等于最大匹配 | 与匹配同阶 | 本节末尾 |
| 最大独立集(二分图) | 总点数 − 最大匹配 | 与匹配同阶 | 本节末尾 |
先解决“怎么知道它是不是二分图”。做法就是把二分图的定义直接翻译成代码:任选一个点染成 0,它的邻居全染成 1,邻居的邻居再染回 0……如果某条边的两端被迫同色,这个图就不是二分图。
| 组成 | 含义 / 格式要求 |
|---|---|
col[v] 的取值 | −1 表示未染色,0 与 1 表示两种颜色。不要用 0 表示“未染色”:第一个连通块的点会被染成 0,于是它永远看起来像没染过,BFS 会反复入队。 |
col[u] ^ 1 | 按位异或 1 就是 0↔1 互换。比写 1 - col[u] 少一次减法,语义也更像“取反”。 |
冲突判据 | col[v] == col[u] 且 v 已染色,说明同一条边的两端同色,绕出来的环长度是奇数,图不是二分图,立刻返回 false。 |
遍历所有点 | 图不一定连通。只从 1 号点染色,其余连通块全是 −1,判定结果随机。外层必须 for (int i = 1; i <= n; ++i) if (col[i] == -1) 开一次 BFS。 |
初始化 | memset(col, -1, sizeof col)。全局数组默认是 0,不 memset 就全部“已染成 0 色”,第一次判断就全错。 |
复杂度 | 每个点入队一次,每条边正反各看一次,O(n + m)。这一步比匹配本身便宜一个数量级。 |
int col[10005];
// 对 s 所在的连通块染色,冲突返回 false
bool paint(int s) {
queue<int> q;
q.push(s);
col[s] = 0;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int e = head[u]; e; e = nxt[e]) {
int v = to[e];
if (col[v] == -1) { col[v] = col[u] ^ 1; q.push(v); }
else if (col[v] == col[u]) return false; // 同色相邻
}
}
return true;
}判断顺序不能反:先看“未染色”再染色,否则新染的点会被自己判成冲突。
自环(u 连 u)一定让图为非二分图——同一个点与自己相邻,两端必然同色,代码天然能判出来。
#include <bits/stdc++.h>
using namespace std;
const int N = 10005;
const int M = 100005;
int head[N], nxt[M << 1], to[M << 1], ecnt;
int col[N]; // -1 未染色,0 / 1 两种颜色
void addEdge(int u, int v) {
to[++ecnt] = v; nxt[ecnt] = head[u]; head[u] = ecnt;
}
// 对 s 所在连通块染色;能二染色返回 true
bool paint(int s) {
queue<int> q;
q.push(s);
col[s] = 0;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int e = head[u]; e; e = nxt[e]) {
int v = to[e];
if (col[v] == -1) { // 没染过:染成相反色
col[v] = col[u] ^ 1;
q.push(v);
} else if (col[v] == col[u]) { // 同一条边两端同色 → 存在奇环
return false;
}
}
}
return true;
}
int main() {
int n, m;
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v; scanf("%d %d", &u, &v);
addEdge(u, v); addEdge(v, u); // 无向边,存两份
}
memset(col, -1, sizeof col); // ★ 必须先置 -1
for (int i = 1; i <= n; ++i) { // 图可能不连通,每块各开一次
if (col[i] != -1) continue;
if (!paint(i)) { printf("No\n"); return 0; }
}
printf("Yes\n");
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
memset(col, -1, sizeof col); | ★ 全局数组的默认值是 0,而 0 是一种合法颜色。不清成 −1,整个图会被当成“已染成 0 色”,第一条边就报冲突。 |
for (int i = 1; i <= n; ++i) | 外层循环负责所有连通块。二分图可以是“一片森林”,只从 1 号点出发会漏掉其它块。 |
if (col[v] == -1) | 先判未染色。若先判“同色”再判“未染色”,新点会被当成冲突。 |
col[v] = col[u] ^ 1; | 异或 1 取反。整张图一旦起点颜色定了,其余所有点的颜色都被唯一确定。 |
else if (col[v] == col[u]) | 已染色且相同 = 冲突。此时 u、v 到 BFS 根的路径长度奇偶相同,加上边 (u,v) 正好构成奇环。 |
nxt[M << 1], to[M << 1] | 链式前向星每条无向边存两份,这两个数组要开 2m。开成 m 会在存反向边时越界。 |
判定解决“结构对不对”,匹配解决“怎么配最多”。下面这道模板题把问题剥到最干净:给一个二分图和若干条允许的配对,问最多能配出几对。
给定一个二分图,左部有 n 个点、右部有 m 个点,共 e 条边(每条边从左侧某点连到右侧某点)。求这个二分图的最大匹配数。
输入:第一行三个整数 n、m、e(1 ≤ n, m ≤ 500,1 ≤ e ≤ 5×104)。接下来 e 行,每行两个整数 u、v,表示左侧点 u 与右侧点 v 之间有一条边。
输出:一行一个整数,为最大匹配数。
先证明贪心不行。还是上面这 5 条边,按左侧点 1、2、3 的顺序,每人都抢第一个还空着的右点:L1 拿到 R1;L2 只剩 R1 可选,被占,放弃;L3 拿到 R2。结果只能配 2 对。
但最优是 3 对:L1–R2、L2–R1、L3–R3。贪心在 L2 这一步锁死了自己——它不知道“让 L1 换个位置”这件事是可行的。
让位就是增广。L2 想抢 R1 时,先看 R1 的主人 L1 能不能腾地方:L1 除了 R1 还能连 R2,而 R2 空着。于是走出一条路 L2 — R1 — L1 — R2,它的边依次是“未匹配、匹配、未匹配”,两端都是未匹配点。把这条路上匹配与未匹配全部翻转,得到 {L2–R1, L1–R2},匹配数从 1 变成 2,一个也没丢。
这就是 增广路:起点和终点都是未匹配点、边一未匹配一匹配交替的路。翻转一条长度为 2k+1 的增广路,匹配数恰好 +1。Berge 定理说:匹配是最大的 ⟺ 不存在增广路。所以只要对每个左侧点都尝试找一次增广路,找不到就说明它配不上,全部试完就是最大匹配。
对每个左侧点 u:
vis[1..m] = false // 本轮尝试重新开始
dfs(u):
for u 的每个邻点 v:
if vis[v]: continue // 本轮试过了,跳过(否则绕圈)
vis[v] = true
if match[v] == 0 // v 空着:直接配
或 dfs(match[v]) // v 有主人:让主人另找出路
match[v] = u; return true
return false // 所有出路都被堵死复杂度:每个左侧点做一次 dfs,单次最多把所有边扫一遍,总 O(n·E)。n = 500、E = 5×104 时是 2.5×107 次,实测远不到 0.1 秒。
| 组成 | 含义 / 格式要求 |
|---|---|
下标含义 | match 用右侧点编号当下标。因为匈牙利算法的核心是“右侧点被谁占着”,左侧点只是发起方,不需要单独记录。 |
值 0 的含义 | 0 号点保留作“空”。点的编号从 1 开始,于是 match[v] == 0 就能直接当判空用,省掉一次“是否已匹配”的额外数组。 |
初值 | 全局数组默认全 0,天然就是“全部未匹配”,不用手动清空。但多测题目里必须 memset(match, 0, sizeof match),否则上一组数据的匹配会带进下一组。 |
不对称 | 左侧点没有对应的数组。要输出方案(例如问“哪两个人一组”)时,遍历 v 输出 match[v] v 即可,反过来查“左侧 u 配给谁”才需要再造一个数组。 |
何时更新 | 只在 dfs 成功的那一层写 match[v] = u。递归返回时每一层都会写一次,整条增广路上的配对关系因此被一次性翻转。 |
大小 | 按右侧点数开,与左侧点数无关。把左右点数搞混是最常见的数组越界来源。 |
int match[505]; // 右侧最多 500 个点
// 匹配成功后:
// match[R1] = L2 R1 现在归 L2
// match[R2] = L1 R2 现在归 L1
// 未匹配的右侧点:match[R3] == 0
// 输出方案(左侧 u 与右侧 v 配对,v 从 1 到 m)
for (int v = 1; v <= m; ++v)
if (match[v]) printf("%d %d\n", match[v], v);match[v] 存的是“左侧点编号”,不是布尔值。写成 bool match[] 就无法还原配对关系。
每次递归只在成功时写 match[v] = u,写完立刻 return true。所以失败的 dfs 不会留下任何改动——不需要回滚,代码里也没有回滚的语句。
| 组成 | 含义 / 格式要求 |
|---|---|
参数 u | 当前发起请求的左侧点。它可能是一个还没配上的新点,也可能是被“要地”的原配。 |
for 每个邻点 v | 按邻接表遍历 u 的所有候选右侧点。顺序不影响正确性,只影响找到的是哪一组最大匹配。 |
if (vis[v]) continue; | 本轮已经试过 v 就跳过。没有这一行,两个左侧点互相让位时会无限递归(RE:stack overflow)。 |
vis[v] = true; | 标记要放在判断之前。无论这次成功还是失败,v 在本轮都不必再试。 |
match[v] == 0 | v 空着,增广路在这里终结,直接配上。 |
dfs(match[v]) | v 被 match[v] 占着,递归去问它“你能不能让出 v”。这句是整个算法的灵魂:不是抢,而是先安排对方。 |
match[v] = u; return true; | 对方让位成功(或 v 本来就空),u 接手 v,返回 true。 |
return false; | 所有邻点都试完仍失败。此时以 u 为起点没有增广路,调用方换下一个 v。 |
为什么 vis 只在右侧 | 每次进入 dfs(u) 都只可能由“u 当前配对的右侧点”触发,而 u 只配了一个右点,所以左侧点不可能重复进入。右侧点却可能被多个左侧点争抢,必须挡。 |
bool dfs(int u) {
for (int e = head[u]; e; e = nxt[e]) { // 遍历 u 的所有候选
int v = to[e];
if (vis[v]) continue; // 本轮试过了
vis[v] = true;
if (match[v] == 0 || dfs(match[v])) { // v 空着,或原配能让位
match[v] = u; // 翻转这条路上的最后一条边
return true;
}
}
return false;
}|| 是短路求值:match[v] == 0 成立时不会进递归,所以空点那一支是 O(1)。
把 || dfs(match[v]) 删掉,算法立刻退化成贪心,答案偏小(本节样例会输出 2 而不是 3)。
#include <bits/stdc++.h>
using namespace std;
const int NL = 505; // 左侧点数上限
const int NR = 505; // 右侧点数上限
const int ME = 100005; // 边数上限(题给 5×10^4,留一倍余量)
int head[NL], nxt[ME], to[ME], ecnt;
int match[NR]; // match[v]:右侧点 v 配给的左侧点,0 表示空
bool vis[NR]; // 本轮 dfs 中右侧点是否试过
void addEdge(int u, int v) {
to[++ecnt] = v; nxt[ecnt] = head[u]; head[u] = ecnt;
}
bool dfs(int u) {
for (int e = head[u]; e; e = nxt[e]) {
int v = to[e];
if (vis[v]) continue; // 本轮试过,跳过(防绕圈)
vis[v] = true; // 先标记,成功失败都不再试
if (match[v] == 0 || dfs(match[v])) { // v 空着,或原配能另找出路
match[v] = u; // 接手 v
return true;
}
}
return false; // 所有候选都被堵死
}
int main() {
int n, m, e;
scanf("%d %d %d", &n, &m, &e);
for (int i = 1; i <= e; ++i) {
int u, v; scanf("%d %d", &u, &v);
addEdge(u, v); // 只存左侧 -> 右侧的单向边
}
int ans = 0;
for (int u = 1; u <= n; ++u) { // 每个左侧点各发起一次尝试
memset(vis, 0, sizeof vis); // ★ 新一轮必须重新清空
if (dfs(u)) ++ans; // 找到增广路,匹配数恰好 +1
}
printf("%d\n", ans);
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
int head[NL], nxt[ME], to[ME]; | 三个数组按各自的上限开:head 按左侧点数,nxt/to 按边数。边只存单向,所以 nxt/to 开 e 就够,不用 <<1。 |
if (vis[v]) continue; | ★ 本轮已试过 v。缺这一行的症状是 RE(栈溢出)或 TLE,因为两个左侧点会互相“让位”给对方,永不终止。 |
vis[v] = true; | 先标记再看能不能配。放在 if 里面会漏标,同一个 v 被反复尝试。 |
match[v] == 0 | 右侧点 v 还没有主人,u 直接接上——增广路长度为 1,匹配数 +1。 |
|| dfs(match[v]) | 短路:上一句为真就不进来。这句让 v 的原配去别处找位置,是“贪心”与“增广”的唯一区别。 |
match[v] = u; | 接手。递归逐层返回时,这条交替路上的每条匹配边都被改写成新的配对,整体相当于一次翻转。 |
return false; | 以 u 为起点没有增广路。按 Berge 定理,这不影响全局最优,跳过即可。 |
memset(vis, 0, sizeof vis); | ★ 每个左侧点前清空。放在外层循环之外会漏掉大量增广路(答案偏小);放进 dfs 内部会每次递归都清空(指数级变慢)。 |
if (dfs(u)) ++ans; | 每成功一次匹配数恰好 +1,所以 ans 就是最终匹配数,不用最后再统计。 |
addEdge(u, v) 只加一次 | 匈牙利只需要从左侧出发找路,反向边用 match 数组代替。加双向边会让 dfs 反复横跳,复杂度爆掉。 |
匹配算完之后,二分图上还有两个量和它死死绑定。它们不需要额外算法,只要知道定理,就是一次加减法。
| 量 | 公式 | 直觉 |
|---|---|---|
| 最大匹配 M | 匈牙利算法求出 | 最多能配成多少对 |
| 最小点覆盖 | = M | 用最少的点,盖住所有边 |
| 最大独立集 | = 总点数 − M | 选最多的点,使任意两点之间没有边 |
| 最大团(二分图内) | = 2(除非图没有边) | 二分图里堆内无边,团最多一个左点加一个右点 |
点覆盖是“选一批点,使每条边至少有一个端点在选中的集合里”;最小点覆盖是其中点数最少的那个。一般图求最小点覆盖是 NP 难的,但二分图上它恰好等于最大匹配,这就是 König 定理。
构造方法(顺带说明为什么相等):从所有未匹配的左侧点出发做交替 BFS,只走“未匹配边向左→右、匹配边向右→左”,把能到达的点全标记。取“标记过的右点”加“未标记的左点”,就是一个大小等于匹配数的点覆盖。
独立集和点覆盖是互补的:删掉一个点覆盖,剩下的点两两之间不可能有边,否则那条边没被盖住。所以最大独立集 = 总点数 − 最小点覆盖 = n + m − 最大匹配。
做题时的用法:题目问“至少删几个点能让所有边消失” → 最小点覆盖 → 答案就是最大匹配;问“最多能选几个点使它们互不相邻” → 最大独立集 → 答案就是 n + m − 最大匹配。两句话都不用写新代码,只要把匹配数拿来算。
WA,答案偏小 1~2。—— memset(vis, 0, sizeof vis) 写在了外层循环外面,只有第一个左侧点享受到了干净的状态。vis 必须每个 u 清一次。
RE(栈溢出)或 TLE 死循环。—— dfs 里少了 if (vis[v]) continue;。输入 2 2 2 / 1 1 / 2 1 就能一秒复现:两个左侧点互相让位,永不返回。
WA,判定全错。—— 染色时用 0 表示“未染色”。第一个连通块被染成 0 之后永远像没访问过,BFS 反复入队(TLE),或者干脆把整片判成冲突。必须用 −1。
WA,只判了一个连通块。—— 外层少了 for (i = 1..n),图不连通时后面的块根本没检查,非二分图被误判成二分图。
RE,段错误。—— vis、match 误按左侧点数开:右侧点比左侧多时立刻越界。分工是 head 按左侧点数开、match 与 vis 按右侧点数开、nxt 与 to 按边数开,n ≠ m 时混用会立刻暴露。
WA,多测全错。—— 多组数据只清了图没清 match,上一组的配对残留进来,表现为第一组对、后面全错。
TLE。—— 建图时双向加边。匈牙利只从左侧出发,反向边会让 dfs 在两侧之间反复横跳,单次 dfs 从 O(E) 变成指数级。
关掉页面,把 P3386 从空文件敲一遍并提交。
改一改 ①:把 match[v] == 0 || dfs(match[v]) 改成只剩 match[v] == 0,用本节的样例输入,输出从 3 变成 2——这就是纯贪心的极限。
改一改 ②:删掉 if (vis[v]) continue;,输入 2 2 2 / 1 1 / 2 1,观察程序直接崩溃(栈溢出)。恢复这一行,答案立刻变回 2。
改一改 ③:染色代码里把 col 初始化为 0、用 0 表示未染色,喂一个 4 个点 2 条边的二分图,看它是不是卡在 BFS 里出不来。
改一改 ④:在匈牙利代码末尾加一句 printf("%d\n", n + m - ans);,用样例验证最大独立集 = 6 − 3 = 3,再自己构造一个 5 个点 2 条边的图验证一遍。
等价刻画一:能二染色 ⟺ 不含奇环。若图能二染色,任取一个环,颜色必须沿环交替,转一圈回到起点要同色,所以环长必为偶数。反过来,若图无奇环,从任一点出发 BFS,把距离为偶数的点染 0、奇数的染 1:同层的两点之间若有边,它们到根的路径长度奇偶相同,加上这条边就构成奇环,矛盾;相邻两层的点颜色本就不同。所以染色一定能完成。
等价刻画二:二分图的两个点集就是染色的两个颜色类。连通块内一旦确定一个点的颜色,其余所有点的颜色都被唯一确定,所以每个连通块恰好有两种合法染色(整体取反),这就是“取 min(黑点数, 白点数)”只需要比较两次的原因。
Berge 增广路定理:匹配 M 是最大的 ⟺ 不存在 M 增广路。一个方向容易:有增广路就翻转它,|M| 增大,M 不最大。另一个方向:若 M 不最大,取更大的 M′,则对称差 M ⊕ M′ 是一堆交替路和交替环,其中必有一条以 M′ 的边开头和结尾的路,即 M 的增广路。
复杂度账:外层至多 n 次,每次 dfs 里每个右侧点最多被标记一次,标记之后最多再扫一遍它的邻边,所以单次 O(E)。总计 O(nE)。n = 500、E = 5×104:2.5×107 次基本操作,按 108/s 估算约 0.25 秒,实测通常 0.02 秒以内——因为绝大多数 dfs 在第一两个邻点就成功返回了。
König 定理与它的推论:二分图中 最大匹配 = 最小点覆盖(记作 ν = τ)。又因为“点覆盖的补集是独立集”且两者一一对应,所以 最大独立集 = |V| − 最小点覆盖 = |V| − ν。这三个量在二分图上全部可以多项式时间求出,而在一般图上最小点覆盖是 NP 难的——二分图这个限制值钱的地方就在这里。
上界感受:匹配数永远不会超过 min(n, m),因为每个匹配边要占用一个左点和一个右点。所以样例里 3 个点最多配 3 对,答案是 3 时已经达到理论最大值,可以直接判定为最优,不必再验证。
阳光大学有 n 个路口和 m 条双向道路。曹冲要在一些路口放置河蟹来封锁所有道路,规则有两条:每条道路的两个端点至少有一个放了河蟹;同一条道路的两个端点不能都放河蟹(两只河蟹会打架)。
求最少需要多少只河蟹。如果无论如何都无法满足,输出 Impossible。
输入:第一行两个整数 n、m(1 ≤ n ≤ 104,1 ≤ m ≤ 105)。接下来 m 行,每行两个整数 u、v,表示路口 u 与 v 之间有一条道路(可能有重边)。
输出:一行。最少河蟹数,或 Impossible。
把“放”记作 1、“不放”记作 0,那么两条规则合起来,对每条边 (u, v) 意味着什么?(“至少一个 1”加“不能都是 1”等于恰好几个 1?)
于是每条边两端的标记必须不同——这个条件你有现成的算法可以检查。
连通块之间没有边,可以分开算再相加。对每个能二染色的连通块,选黑点还是选白点更省?
对每条边 (u, v),规则要求标记恰好一个是 1、一个是 0。这就是“两端标记不同”,也就是二染色的要求。所以:图必须是二分图,否则输出 Impossible。
对每个连通块做 BFS 染色。连通块内只要起点的颜色定了,其余全被唯一确定,所以合法方案只有两种:选全部黑点,或选全部白点。
设这个连通块黑点 b 个、白点 w 个。选黑点意味着放 b 只河蟹,选白点放 w 只,取 min(b, w) 即可。为什么两种都合法?因为所有边都跨黑白色,每条边恰好被选中一个端点。
各连通块互不影响,答案相加。孤立点没有边要盖,min(1, 0) = 0,自动贡献 0,不用特判。
复杂度 O(n + m)。染色时顺便统计两种颜色的点数,一次 BFS 同时完成判定与计数。
#include <bits/stdc++.h>
using namespace std;
const int N = 10005;
const int M = 100005;
int head[N], nxt[M << 1], to[M << 1], ecnt;
int col[N]; // -1 未染色,0 / 1 两种颜色
int cnt[2]; // 当前连通块里两种颜色的点数
void addEdge(int u, int v) {
to[++ecnt] = v; nxt[ecnt] = head[u]; head[u] = ecnt;
}
// 对 s 所在连通块染色,同时统计两种颜色各多少个;出现奇环返回 false
bool paint(int s) {
queue<int> q;
q.push(s);
col[s] = 0;
cnt[0] = cnt[1] = 0;
while (!q.empty()) {
int u = q.front(); q.pop();
++cnt[col[u]]; // 出队时统计,每个点只算一次
for (int e = head[u]; e; e = nxt[e]) {
int v = to[e];
if (col[v] == -1) { col[v] = col[u] ^ 1; q.push(v); }
else if (col[v] == col[u]) return false; // 同色相邻 → 奇环
}
}
return true;
}
int main() {
int n, m;
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v; scanf("%d %d", &u, &v);
addEdge(u, v); addEdge(v, u); // 双向道路,存两份
}
memset(col, -1, sizeof col); // ★ 未染色必须是 -1
int ans = 0;
for (int i = 1; i <= n; ++i) {
if (col[i] != -1) continue; // 已在别的连通块里染过
if (!paint(i)) { printf("Impossible\n"); return 0; }
ans += min(cnt[0], cnt[1]); // 两种染色方案,取更省的那个
}
printf("%d\n", ans);
return 0;
}| 片段 | 作用 |
|---|---|
int cnt[2]; | 只开两个桶。颜色的取值恰好是 0/1,直接用 cnt[col[u]] 当下标,省掉 if-else 两分支。 |
cnt[0] = cnt[1] = 0; | ★ 每个连通块开始前清零。忘了清就把上一块的点数累加进来,答案偏大。 |
++cnt[col[u]]; | 出队时统计。每个点在入队时就已染好色、且只会入队一次,所以出队计数也是恰好一次;写成入队时统计同样正确,放在出队处只是更不容易漏。 |
col[v] = col[u] ^ 1; | 邻居染相反色。这条边一旦确定,v 的颜色就没有第二种可能。 |
else if (col[v] == col[u]) return false; | 同色相邻只能来自奇环。此时两条规则无法同时满足,直接输出 Impossible 并退出。 |
ans += min(cnt[0], cnt[1]); | 选黑点还是选白点:两者都满足“每条边恰好一个端点被选”,所以取点数少的那个。 |
if (col[i] != -1) continue; | 图不连通。不跳过的话会把同一个连通块重复算两次,答案翻倍。 |
memset(col, -1, sizeof col); | 全局数组默认 0,与颜色 0 冲突。不清成 −1,从 1 号点开始的 BFS 会把自己判成已染色。 |
impossible 或 IMPOSSIBLE:字符串严格比对,大小写不同直接 WA。