利用 H[i] 是团的性质,把每个点 i 对应到团 P(i)={i}∪H[i],用最大前驱与入度的一次比较判定 P(i) 是否被包住,未被标记的点数即极大团个数。
OJ: roj
题目 ID: 10022
难度:提高
标签:图论极大团弦图python
创建: 2026-10-02 18:03
更新: 2026-10-07 11:01
形式化题目
给定有向图
本身是一个团(输入可能含重边)。对点集
- 团:
中任意两点之间都有边(方向为小编号指向大编号),空集也算团; - 可扩团:存在一个点,与
中每个点之间都有边; - 极大团:不是可扩团的团,即不能再加入任何点的团(空团总可以再加入任意一点,所以极大团都是非空团)。
求
样例输入对应的有向图如下,它同时演示了题设性质:
digraph G {
rankdir=LR;
node [shape=circle];
1 -> 2;
1 -> 3;
2 -> 3;
2 -> 4;
3 -> 4;
}
样例的两个极大团是
正解
思路
朴素做法。 枚举所有点集,逐一判断“是否为团、是否还能扩大”,复杂度
第一步:把每个团挂到它的最大点上。 对点
由题设
由此得到一一对应:任取团
第二步:包含关系只需向后检查。 若
第三步:用入度比较代替集合包含(关键)。 记
- 充分:若
,由 (1) 只能是 且 ,于是 ,即 的团被 吸收。 - 必要:若
(此时 、边 ),则 ,且 的元素都小于 、不会等于 ,故 。从 出发令 :由 (1) 同样的理由 ,而 不会削掉 的任何元素,故归纳地 。该序列严格递减且始终满足 (因为 ),有限步后必有某步 ,此时 , 按判据被标记。
于是得到充要判据:
第四步:重边先去重。 判据里的 a<<20|b(
扫描与答案。 按
| 扫描到 |
|||||
|---|---|---|---|---|---|
| 1 | 0 | 无前驱,跳过 | |||
| 2 | 1 | 1 | |||
| 3 | 2 | 2 | |||
| 4 | 2 | 3 |
被标记的是 1、2:
实现里 packed 是压成 a<<20|b、排序去重后的边表,pred_cnt/pred_max 依次存 covered 存上表最后一列的标记结果,三者与上述推导一一对应。
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 18:30
# update_at: 2026-10-02 18:30
import sys
from array import array
SHIFT = 20 # 点编号 n<=1e6<2^20,a<<20|b 把一条边压成一个整数,排序后顺带去重
SPAN = 1 << SHIFT
def solve() -> None:
src = sys.stdin.buffer
n, m = map(int, next(src).split())
packed = [0] * m
for i in range(m):
a, b = map(int, next(src).split())
packed[i] = a << SHIFT | b # a 占高位、b 占低 20 位,按 (a, b) 字典序排序
packed.sort()
# 排序后相同边相邻:pred_cnt 记去重后的前驱数 |H[i]|,pred_max 记最大前驱 last[i]
pred_cnt = array("i", [0]) * (n + 1)
pred_max = array("i", [0]) * (n + 1)
seen = -1
for key in packed:
if key != seen:
seen = key
b = key & (SPAN - 1)
pred_cnt[b] += 1
pred_max[b] = key >> SHIFT # 同一个 b 的 key 按 a 升序,最后一次即最大前驱
# P(i)={i}∪H[i] 是包含 i 的最大团。H[i]\{last[i]} ⊆ H[last[i]] 恒成立,
# 所以 |H[i]|>|H[last[i]]| 等价于 H[i]=H[last[i]]∪{last[i]}=P(last[i]),
# 即 P(last[i]) 整个被 i 吸收 → last[i] 对应的团被包住,不是极大团。
covered = bytearray(n + 1)
for i in range(1, n + 1):
j = pred_max[i]
if j and pred_cnt[i] > pred_cnt[j]:
covered[j] = 1
print(n - sum(covered))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-04 23:06
* update_at: 2026-10-04 23:06
*/
// main.cpp:roj 10022 网络
// 题设保证每个点的前驱集合 H[i] 是团,即编号 1..n 是弦图的一个完美消除序列。
// 记 P(i) = {i} ∪ H[i],它是包含 i 的最大团;极大团恰好是那些不被其它 P(j) 包住的 P(i)。
// 若 j = last[i](i 的最大前驱),由 H[i] 是团可得 H[i]\{j} ⊆ H[j],
// 于是 |H[i]| > |H[j]| 等价于 P(j) ⊆ P(i),即 j 对应的团被 i 吸收。
// 所以按 i 扫一遍、用一次整数比较标记被吸收的 j,答案就是 n 减去被标记点的个数。
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long ll;
const ll MAXN = 1000005; // n <= 1e6
const ll MAXM = 1000005; // m <= 1e6
const ll SHIFT = 20; // n <= 1e6 < 2^20,把一条边压成 a<<SHIFT|b
ll n, m;
ll packed_edge[MAXM]; // 每条边压缩为 a<<SHIFT|b,排序后既去重、又保证同一终点的前驱按起点升序
int pred_cnt[MAXN]; // pred_cnt[i] = |H[i]|,去重后 i 的不同前驱个数(<= 1e6,用 int 省内存)
int pred_max[MAXN]; // pred_max[i] = max H[i],即 i 的最大前驱 last[i],0 表示没有前驱
char covered[MAXN]; // covered[j] = 1 表示 P(j) 被更大的团包住,不是极大团(标记只有 0/1,用 char)
const int BUF_SIZE = 1 << 20;
char in_buf[BUF_SIZE]; // fread 输入缓冲:最多 2e6 个整数,cin 会超时
int buf_pos = 0, buf_len = 0;
// 从缓冲区取一个字符,缓冲区读空时用 fread 续读;输入结束返回 -1。
int read_char() {
if (buf_pos == buf_len) {
buf_len = fread(in_buf, 1, BUF_SIZE, stdin);
buf_pos = 0;
if (buf_len <= 0) return -1;
}
return in_buf[buf_pos++];
}
// 快读一个非负整数。
ll read_int() {
int c = read_char();
while (c <= ' ' && c != -1) c = read_char();
ll x = 0;
while (c > ' ') {
x = x * 10 + (c - '0');
c = read_char();
}
return x;
}
void read_input() {
n = read_int();
m = read_int();
for (ll i = 0; i < m; i++) {
ll a = read_int();
ll b = read_int();
packed_edge[i] = (a << SHIFT) | b; // a 占高位、b 占低 20 位,按 (a,b) 字典序排序
}
}
int main() {
read_input();
sort(packed_edge, packed_edge + m);
// 排序后相同边相邻,跳过重复边完成去重;
// 同一个终点 b 的边按起点 a 升序出现,最后一次写入的 a 就是最大前驱。
ll seen = -1;
for (ll i = 0; i < m; i++) {
if (packed_edge[i] == seen) continue;
seen = packed_edge[i];
int b = packed_edge[i] & ((1 << SHIFT) - 1);
pred_cnt[b] = pred_cnt[b] + 1;
pred_max[b] = packed_edge[i] >> SHIFT;
}
// P(i) = {i} ∪ H[i] 是包含 i 的最大团,极大团与未被包住的 P(i) 一一对应。
// 若 last[i] = j 且 |H[i]| > |H[j]|,则 H[i] = H[j] ∪ {j},有 P(j) ⊆ P(i),j 被吸收。
for (ll i = 1; i <= n; i++) {
int j = pred_max[i];
if (j != 0 && pred_cnt[i] > pred_cnt[j]) {
covered[j] = 1;
}
}
ll ans = 0;
for (ll i = 1; i <= n; i++) {
if (covered[i] == 0) ans = ans + 1;
}
printf("%lld\n", ans);
return 0;
}复杂度
时间复杂度
总结
本题的钥匙是题设“