目录
1. 题目核心分析
问题描述: 给出
转化: 这是一个典型的 最大独立集 (Maximum Independent Set) 问题。
- 节点:每一条线段看作一个节点。
- 冲突(边):如果两条线段相交,则在它们之间连一条边。
- 目标:选出最多的节点,使得任意两个节点之间没有边相连。
2. 建模思路:二分图
通常情况下,求一般图的最大独立集是 NP-Hard 问题。但本题有一个特殊的性质:
题目保证初始示意图中任意两条水平线段不会相交,任意两条垂直线段也不会相交。
这意味着:
- 冲突只可能发生在 水平线段 和 垂直线段 之间。
- 水平线段内部没有边。
- 垂直线段内部没有边。
这正是一个二分图 (Bipartite Graph)!
- 左部点集 (
):所有的水平线段。 - 右部点集 (
):所有的垂直线段。 - 边 (
):若水平线段 与垂直线段 相交,连边 。
3. 数学原理:最大独立集与最大匹配
在二分图中,有著名的 Kőnig 定理 推论:
而在二分图中:
因此:
算法流程:
- 分类:将输入的
条线段分为“水平”和“垂直”两组。 - 建图:
- 设立源点
和汇点 。 每个水平线段,容量 1。 - 每个垂直线段
,容量 1。 - 枚举每一对(水平线段
,垂直线段 )。如果它们在几何上相交,建立边 ,容量 1。
- 设立源点
- 求解:运行最大流算法(Dinic)求出最大匹配数。
- 输出:
。
4. 几何相交判断
设水平线段为
它们相交的充要条件是:
- 垂直线段的
坐标在水平线段的 范围内: - 水平线段的
坐标在垂直线段的 范围内:
5. 复杂度分析
- 节点数:
。 - 边数:最坏情况下水平和垂直线段两两相交,约
。 - Dinic 复杂度:对于二分图匹配,Dinic 的复杂度为
,在此数据规模下瞬间完成。
样例分析
- 几何转图论:题目中的线段分为“水平”和“垂直”两类。
- 题目保证水平线段之间不相交,垂直线段之间不相交。
- 冲突(相交)只发生在水平线段和垂直线段之间。
- 二分图性质:如果我们把每个线段看作图中的一个节点,如果两条线段相交,就在它们之间连一条边。由于同类线段不相交,这个图必然是一个二分图(左边是水平线段集合,右边是垂直线段集合)。
- 目标:选出最多的线段,使得它们互不相交。这等价于在二分图中寻找最大独立集。
- 公式:在二分图中,
最大独立集 = 节点总数 - 最大匹配数。
样例数据分析
输入:
3
4 5 10 5 (线段1, 水平)
6 2 6 12 (线段2, 垂直)
8 3 8 5 (线段3, 垂直)
相交判断:
- 线段1 (H):
- 线段2 (V):
。交点 。由于 且 ,相交。 - 线段3 (V):
。交点 。由于 且 (端点接触),相交。
结论:线段1 与 线段2、线段3 都冲突。如果我们选了线段1,就不能选2和3(数量为1)。如果我们不选线段1,就可以同时选2和3(数量为2)。最优解是选2和3。
图形绘制 (Graphviz)
这个图形展示了线段之间的冲突关系。
- 红色边:表示两个线段在空间上相交(冲突)。
- 绿色节点:表示最优解中选择的线段。
- 灰色节点:表示为了最优解而舍弃的线段。
graph IntersectionGraph {
fontname="Helvetica,Arial,sans-serif";
node [fontname="Helvetica,Arial,sans-serif"];
edge [fontname="Helvetica,Arial,sans-serif"];
// 图形布局设置
layout=dot;
rankdir=LR; // 从左到右布局,体现二分图结构
labelloc="t";
label="P3033 Intersection Graph (Bipartite)\nSolution: Max Independent Set = {Seg 2, Seg 3}";
// 定义节点样式
node [shape=record, style="filled,rounded", margin=0.2];
// 子图:水平线段集合
subgraph cluster_horizontal {
label = "Horizontal Segments (Set A)";
style = dashed;
color = blue;
// 线段1与另外两个都冲突,如果要保留最多的,只能舍弃它
// 样式:灰色,表示被移除(Vertex Cover的一部分)
node [fillcolor="#e0e0e0", color="#666666", fontcolor="#666666"];
Seg1 [label="{Segment 1 | (4,5) to (10,5) | Horizontal}"];
}
// 子图:垂直线段集合
subgraph cluster_vertical {
label = "Vertical Segments (Set B)";
style = dashed;
color = red;
// 这两个线段互不冲突,且去掉了Seg1后,它们都变成了孤立点
// 样式:绿色,表示被选择(Independent Set的一部分)
node [fillcolor="#daffda", color="#2e8b57", fontcolor="#000000"];
Seg2 [label="{Segment 2 | (6,2) to (6,12) | Vertical}"];
Seg3 [label="{Segment 3 | (8,3) to (8,5) | Vertical}"];
}
// 边:表示几何上的相交(冲突)
edge [color="#ff4444", penwidth=2, style=solid];
// 关系描述
Seg1 -- Seg2 [label="intersects at\n(6, 5)"];
Seg1 -- Seg3 [label="intersects at\n(8, 5)"];
}
图形解读
- 左侧 (Horizontal):只有
Segment 1。 - 右侧 (Vertical):有
Segment 2和Segment 3。 - 连线 (Edges):
Segment 1连向Segment 2,因为它们在点相交。 Segment 1连向Segment 3,因为它们在点相交。
- 解的表示:
- 这是一个 “1对2” 的结构。
- 为了让选出的点互不相连(互不相交),我们有两种选择:
- 方案A:选
Segment 1(排除 2, 3)。总数 1。 - 方案B:选
Segment 2和Segment 3(排除 1)。总数 2。
- 方案A:选
- 图中将 Seg 2和 Seg 3 标记为绿色,表示这是最优解(Max Independent Set)。
对应的 Mermaid 代码 (备选)
如果你只需要查看简单的拓扑结构,可以使用 Mermaid:
graph LR
subgraph Horizontal [水平线段]
H1(Seg 1: 4,5 to 10,5)
style H1 fill:#ccc,stroke:#333,stroke-dasharray: 5 5
end
subgraph Vertical [垂直线段]
V1(Seg 2: 6,2 to 6,12)
V2(Seg 3: 8,3 to 8,5)
style V1 fill:#bfb,stroke:#333
style V2 fill:#bfb,stroke:#333
end
H1 -- "(6,5)" --- V1
H1 -- "(8,5)" --- V2
linkStyle 0 stroke:red,stroke-width:2px;
linkStyle 1 stroke:red,stroke-width:2px;
总结
这道题的核心在于:
- 根据坐标建立二分图(建图复杂度
)。 - 求二分图最大匹配(匈牙利算法或网络流)。
- 答案 =
- 最大匹配数。 - 本例中
,最大匹配数为1(边 H1-V1或H1-V2只能选一条),所以答案为。
- 本例中
代码
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github : https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* date: 2026-01-13 20:56:51
*/
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6+5; // 点
const int maxe = 2e6+5; // 边 (注意:要是题目边数的2倍)
const long long INF = 1e18;
int n,m;
int s,t; // 源点 汇点
int a[maxn];
// 存图的模板
//oisnip_begin code/graph/linklist.cpp 内容开始
// const int maxn = 1e6+5;
// const int maxe = 1e6+5;
struct linkList {
typedef struct {int u,v,w,next;} edge;
edge e[maxe];
int h[maxn],edge_cnt=0;
linkList(){
reset();
}
void reset() {
edge_cnt=0;
memset(h,-1,sizeof(h));
}
//遍历点u 周围点
template<typename U>
void for_each(int u,U func){
for(int i = h[u] ; i !=-1;i = e[i].next)
func(e[i].u,e[i].v,e[i].w); //u v w
}
void add(int u,int v,int w=0){
e[edge_cnt] = {u,v,w,h[u]};
h[u] = edge_cnt++;
}
void add2(int u,int v,int w=0){
add(u,v,w);
add(v,u,w);
}
//下标访问
edge& operator[](int i){ return e[i]; }
// 参考 语法糖
// https://en.cppreference.com/w/cpp/language/range-for.html
#ifdef __cpp_range_based_for
// C++ 模板 和 策略模式(Policy) 来消除重复代码。
// 我们可以定义一个通用的迭代器模板,通过传入不同的“提取器(Getter)”来决定 operator* 返回什么。
// === 1. 定义数据提取策略 (核心区别) ===
// 策略A: 获取整条边 (对应原本的 Iterator)
struct UseEdge {
using ReturnType = edge&; // 定义返回类型
static ReturnType get(linkList* p, int i) { return p->e[i]; }
};
// 策略B: 只获取邻接点v (对应原本的 AdjIterator)
struct UseAdj {
using ReturnType = int; // 定义返回类型
static ReturnType get(linkList* p, int i) { return p->e[i].v; }
};
// === 2. 通用迭代器模板 (复用逻辑) ===
template<typename Getter>
struct BaseIterator {
int i; // 边的编号
linkList* p; // linkList指针
BaseIterator(linkList* p, int i) : p(p), i(i) {}
// 通用的遍历逻辑
BaseIterator& operator++() { i = p->e[i].next; return *this; }
bool operator!=(const BaseIterator& oth) { return i != oth.i; }
// 差异化逻辑:委托给 Getter 处理
typename Getter::ReturnType operator*() { return Getter::get(p, i); }
};
// 定义具体的迭代器别名
using Iterator = BaseIterator<UseEdge>;
using AdjIterator = BaseIterator<UseAdj>;
// === 3. 通用范围类模板 ===
template<typename IterT>
struct BaseRange {
int start;
linkList* p;
BaseRange(linkList* p, int start) : p(p), start(start) {}
IterT begin() { return IterT(p, p->h[start]); }
IterT end() { return IterT(p, -1); }
};
// === 4. 接口语法糖 ===
// usage: for(auto& e : list(u))
BaseRange<Iterator> operator()(int u) { return BaseRange<Iterator>(this, u); }
// usage: for(int v : list.adj(u))
BaseRange<AdjIterator> adj(int u) { return BaseRange<AdjIterator>(this, u); }
#endif
} e;
//oisnip_end code/graph/linklist.cpp 内容结束
// Dinic算法最大流模板 - 基于linkList存图
struct Dinic {
vector<int> level, cur; // level: BFS分层, cur: 当前弧优化
int n; // 节点数
void init(int n) {
// 重置linkList
e.edge_cnt = 0;
memset(e.h, -1, sizeof(e.h));
level.resize(n+5);
cur.resize(n+5);
this->n = n;
}
// 添加边:从u到v,容量为cap
// 使用技巧:正向边和反向边的索引相差1,通过异或1来找到对应边
void addEdge(int u, int v, int cap) {
e.add(u, v, cap); // 正向边,w字段存储容量
e.add(v, u, 0); // 反向边,容量为0
}
// BFS分层,构建层次图
bool bfs(int s, int t) {
fill(level.begin(), level.end(), -1);
queue<int> q;
level[s] = 0;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
// 使用linkList的遍历方式
for(int i = e.h[u] ; ~i ;i = e[i].next) {
int v = e[i].v, cap = e[i].w;
if (cap > 0 && level[v] < 0) {
level[v] = level[u] + 1;
q.push(v);
}
}
}
return level[t] >= 0; // 返回是否能到达汇点
}
// DFS寻找增广路
// 到达点u流量为preFlow
// 计算从点u出发的最大流,到达点t
// 本质是一个DAG 上的dp
long long dfs(int u, int t, long long preFlow) {
if (u == t || preFlow == 0) return preFlow;
long long flow = 0;
// 当前弧优化:从cur[u]开始遍历
for (int& cid = cur[u]; cid != -1; cid = e[cid].next) {
auto& edge = e[cid]; // 当前弧优化
int to = edge.v;
long long cap = edge.w;
if (level[u] + 1 != level[to] || cap <= 0) continue;
long long tr = dfs(to, t, min(preFlow, cap));
// 更新容量
e[cid].w -= tr ; // 正向边容量减少
e[cid ^ 1].w += tr; // 反向边容量增加(利用异或找到反向边)
flow += tr;
preFlow -= tr;
if (preFlow == 0) break;
}
// 炸点优化
// 剪枝:取掉增广完毕的点
if (flow == 0) level[u] = -1;
return flow;
}
// 求从s到t的最大流
long long maxFlow(int s, int t) {
long long flow = 0;
while (bfs(s, t)) { // 能够分层
// 当前弧优化重置:将cur设置为每个节点的第一条边
for (int i = 0; i <= n; i++) {
cur[i] = e.h[i]; // 使用linkList的operator()获取head[i]
}
// 多路增广
flow += dfs(s, t, LLONG_MAX);
}
return flow;
}
} dinic;
void init(){
std::cin >> n >> m;
std::cin >> s >> t;
dinic.init(n);
for (int i = 0; i < m; i++) {
int u, v,cap;
cin >> u >> v >> cap;
//添加流量,自动添加反向边
dinic.addEdge(u, v, cap);
}
}
// 使用示例:
int main() {
ios::sync_with_stdio(0); cin.tie(0);
init();
cout << dinic.maxFlow(s,t) << "\n";
return 0;
}
/*
复杂度分析:
- 时间复杂度:O(V²E) 一般情况下表现很好,对于单位容量网络是O(min(V^(2/3), E^(1/2)) * E)
- 空间复杂度:O(V + E)
使用说明:
1. 创建Dinic实例:Dinic dinic(n);
2. 添加边:dinic.addEdge(u, v, cap);
3. 求最大流:long long flow = dinic.maxFlow(source, sink);
注意事项:
- 节点编号从0开始
- 如果题目给的是1-indexed,记得转换
- 容量使用long long防止溢出
- 无向边需要添加两条有向边
*/