持股累加后用不动点迭代求控制闭包:每轮把所有已控制公司持有的股份汇总,超过 50% 即纳入控制,直到不再新增,O(V^3) 内出全部控制对。
OJ: roj
题目 ID: 2034
难度:普及+/提高-
标签:图论传递闭包不动点模拟usacopython
创建: 2026-10-01 04:15
更新: 2026-10-07 13:50
形式化题目
公司编号
在编号上定义一个二元关系「控制」
(自己控制自己); 直接持有 的股份大于 ; - 存在若干公司
( ),每个都满足 ,且它们直接持有 的股份之和大于 。
要求输出所有
正解
思路
朴素做法。 最直接的想法是:先把"直接持股超过
但这个做法漏掉了规则 3 的真正威力:控制权来自股份的加总,而不是来自一条可以直接走的边。看一个反例:
1 4 30
2 4 30
1 2 60公司
关键观察:把它看成一个不动点。 换个角度,别去区分"直接控制"和"间接控制",而是让规则 3 自己描述一个闭包。对固定的
其中
这个算子有三个性质,合起来保证了迭代可用:
- 单调:
。持股只做加法,多认识一家公司只会让汇总值变大。 - 扩张:
,因为 且旧成员靠规则 1 保留。 可以逐轮吃进去:这正是题目允许的"边连锁边纳股份"——本轮刚控制到的公司,它持有的股份本轮就能计入。
于是从
用上一节的例子走一遍:
连锁为什么不用管顺序。 有人会担心"先看
样例验证。 样例数据是 1 2 80、2 3 80、3 1 20、共
| 迭代过程 | 得到的 |
|
|---|---|---|
| 1 | ||
| 2 | ||
| 3 |
去掉"控制自己"的对角线,正好输出 1 2、1 3、2 3,与样例一致。
实现要点。
- 公司编号上限是
,但真实数据里出现的最大编号可能更小。代码取输入中出现过的最大编号 作为枚举上界即可,答案排序天然由 for h升序、for s升序保证。 - 同一条持股边可能给多次,
own[i][j] += p必须累加;不累加会把当成 ,直接漏掉上面那个反例。 - 判据是严格大于
,不是大于等于:正好 不算控制。 - 输出没有答案时不打印任何行;有答案时逐行
h s。
代码
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-01 04:15
# update_at: 2026-10-01 04:15
import sys
MAXV = 100 # 公司编号上限:题面保证 i, j, p 都在 1..100
MAJORITY = 50 # 控股线:汇总持股要「大于 50%」才算控制
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
lines: list[tuple[int, int, int]] = []
for _ in range(next(data)):
i, j, p = next(data), next(data), next(data)
lines.append((i, j, p))
max_id = max((max(i, j) for i, j, _ in lines), default=0)
v = max(max_id, 1) # 公司实际编号范围:控制关系与自身控制都在这个范围内
# own[a][b] = 公司 a 直接持有 b 的股份百分比;没有股份的边留 0,对不等式无影响
own = [[0] * (v + 1) for _ in range(v + 1)]
for i, j, p in lines:
own[i][j] += p # 题面允许多组 (i, j, p) 描述同一条持股边,必须累加
ctrl = [[False] * (v + 1) for _ in range(v + 1)]
for h in range(1, v + 1):
ctrl[h][h] = True # 条件一:控制自己
# 关系会连锁:本轮新控制到的公司,它的股份本轮立刻能纳进来。
# 所以反复扫描,直到某一轮没有新增,才说明这条链闭包已经取完。
while True:
added = False
for b in range(1, v + 1):
if ctrl[h][b]:
continue
# 汇总所有受 h 控制的公司所持有的 b 股份(含 h 自己)
stake = sum(own[c][b] for c in range(1, v + 1) if ctrl[h][c])
if stake > MAJORITY:
ctrl[h][b] = True
added = True
if not added:
break
out = [f"{h} {s}" for h in range(1, v + 1) for s in range(1, v + 1) if ctrl[h][s] and h != s]
sys.stdout.write("\n".join(out) + ("\n" if out else ""))
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-06 09:25
* update_at: 2026-10-06 09:25
*/
#include <cstdio>
typedef long long ll;
const int MAXN = 105; // 公司编号上限:题面保证 i, j, p 都在 1..100
const ll MAJORITY = 50; // 控股线:汇总持股要「大于 50%」才算控制
int n; // 输入的三对数数量
int v; // 实际出现的最大公司编号,枚举上界
ll own[MAXN][MAXN]; // own[a][b] = 公司 a 直接持有 b 的股份百分比(多条边累加)
bool ctrl[MAXN][MAXN]; // ctrl[h][s] = 公司 h 是否控制公司 s
// 对固定的 h,从 {h} 出发反复做不动点迭代,求出 h 控制的全部公司。
// 每一轮把「当前已知被 h 控制的公司」持有的股份汇总一次,超过 50% 就纳入;
// 关系会连锁:本轮新控制到的公司,它的股份下一轮立刻能计入。
void solve_for(int h) {
ctrl[h][h] = true; // 条件一:控制自己
while (true) {
bool added = false; // 本轮是否有新公司被控制
for (int s = 1; s <= v; s++) {
if (ctrl[h][s])
continue;
// 汇总所有受 h 控制的公司(含 h 自己)持有的 s 的股份
ll stake = 0;
for (int c = 1; c <= v; c++)
if (ctrl[h][c])
stake += own[c][s];
if (stake > MAJORITY) { // 严格大于 50%:正好 50% 不算控制
ctrl[h][s] = true;
added = true;
}
}
if (!added) // 一整轮没有新增,控制闭包已经取完
break;
}
}
int main() {
scanf("%d", &n);
for (int k = 1; k <= n; k++) {
int i, j;
ll p;
scanf("%d %d %lld", &i, &j, &p);
own[i][j] += p; // 同一条持股边可能在输入里出现多次,必须累加
if (i > v)
v = i;
if (j > v)
v = j;
}
if (v < 1)
v = 1; // 输入为空时也要有一个非空枚举范围
for (int h = 1; h <= v; h++)
solve_for(h);
// 输出所有 h != s 的控制对,双重循环保证按 (h, s) 字典序
for (int h = 1; h <= v; h++)
for (int s = 1; s <= v; s++)
if (ctrl[h][s] && h != s)
printf("%d %d\n", h, s);
return 0;
}复杂度
对每个
在上述实现里,汇总那一步是"concom8.in 不到
- 空间复杂度:持股矩阵与控制矩阵各
个布尔/整数, 。
总结
- 题意核心:控制不是简单的传递闭包,而是"已控制集合所持股权的汇总超过
"。先做持股图的传递闭包会漏解。 - 关键一步:把三条规则合写成单调扩张算子
,从 迭代到不动点即得 的控制集。 - 连锁的含义:本轮新控制到的公司,其持股本轮即可计入;单调性保证迭代顺序不影响最终集合。
- 易错点:多条同向持股边要累加;判据是严格大于
;输出不含控制自己的对,且保持 字典序。