题目解析
这道题目是典型的 二分图最大匹配 问题,或者更准确地说是 二分图多重匹配 问题(因为右边的每个节点容量为2)。
- 左侧节点(人):有
个人,每个人都有两个想坐的排数选择。 - 右侧节点(座位排):有
排座位,每排座位最多能坐 2 个人。 - 边(Edge):如果某个人想坐第
排,就在该人与第 排之间连一条边。 - 目标:选择尽可能多的边,使得每个人最多连一条边(只能坐一个位置),每排最多连两条边(每排坐两人)。
样例数据分析
输入:
4 (N=4, 所以有4排座位,8个人)
1 2 (Person 1: 想坐 Row 1 或 Row 2)
1 3 (Person 2: 想坐 Row 1 或 Row 3)
1 2 (Person 3: 想坐 Row 1 或 Row 2)
1 3 (Person 4: 想坐 Row 1 或 Row 3)
1 3 (Person 5: 想坐 Row 1 或 Row 3)
2 4 (Person 6: 想坐 Row 2 或 Row 4)
1 3 (Person 7: 想坐 Row 1 或 Row 3)
2 3 (Person 8: 想坐 Row 2 或 Row 3)
输出:
7
这意味着最多有7个人能坐到满意的座位。
图形绘制 (Graphviz)
为了清晰展示,我将使用网络流的视角来构建这个图:
- 源点 S 连接所有人,容量为1(每个人只能坐一个位置)。
- 汇点 T 连接所有排,容量为2(每排能坐两人)。
- 人与排之间容量为1。
- 我在图中会标出一种可能的最大流路径(红色边),使得总流量为7。
digraph G {
rankdir=LR;
nodesep=0.5;
ranksep=0.8;
// 定义节点样式
node [shape=circle, width=0.6, fixedsize=true, fontname="Arial"];
// 特殊节点
S [label="S", style=filled, fillcolor=lightgray, shape=doublecircle];
T [label="T", style=filled, fillcolor=lightgray, shape=doublecircle];
// 左侧:2N个人 (P1-P8)
subgraph cluster_people {
label = "参赛者 (2N=8)";
style = dashed;
color = blue;
node [style=filled, fillcolor="#e1f5fe"];
P1 [label="P1"];
P2 [label="P2"];
P3 [label="P3"];
P4 [label="P4"];
P5 [label="P5"];
P6 [label="P6"];
P7 [label="P7"];
P8 [label="P8"];
}
// 右侧:N排座位 (R1-R4)
subgraph cluster_rows {
label = "座位排 (N=4)\n每排容量: 2";
style = dashed;
color = green;
node [style=filled, fillcolor="#e8f5e9", shape=box];
R1 [label="Row 1"];
R2 [label="Row 2"];
R3 [label="Row 3"];
R4 [label="Row 4"];
}
// 源点到人的边 (容量1) - 省略文字标签以保持整洁,默认选中为红色
edge [color=gray, arrowsize=0.6];
S -> P1 [color=red, penwidth=2];
S -> P2 [color=red, penwidth=2];
S -> P3 [color=red, penwidth=2];
S -> P4 [color=red, penwidth=2];
S -> P5 [color=black]; // P5 未被选中 (这是假设的未选中者)
S -> P6 [color=red, penwidth=2];
S -> P7 [color=red, penwidth=2];
S -> P8 [color=red, penwidth=2];
// 人的选择 (根据输入)
// 红色粗线表示被选中的匹配方案
// P1: 1, 2. Selected: Row 2
P1 -> R1;
P1 -> R2 [color=red, penwidth=2];
// P2: 1, 3. Selected: Row 1
P2 -> R1 [color=red, penwidth=2];
P2 -> R3;
// P3: 1, 2. Selected: Row 1
P3 -> R1 [color=red, penwidth=2];
P3 -> R2;
// P4: 1, 3. Selected: Row 3
P4 -> R1;
P4 -> R3 [color=red, penwidth=2];
// P5: 1, 3. Skipped (No available seats in preference)
P5 -> R1;
P5 -> R3;
// P6: 2, 4. Selected: Row 4
P6 -> R2;
P6 -> R4 [color=red, penwidth=2];
// P7: 1, 3. Selected: Row 3
P7 -> R1;
P7 -> R3 [color=red, penwidth=2];
// P8: 2, 3. Selected: Row 2
P8 -> R2 [color=red, penwidth=2];
P8 -> R3;
// 座位排到汇点的边 (容量2)
// 观察上面的分配:
// R1 被 P2, P3 选中 (满)
// R2 被 P1, P8 选中 (满)
// R3 被 P4, P7 选中 (满)
// R4 被 P6 选中 (半满)
edge [label="cap=2"];
R1 -> T [color=red, penwidth=2, label="2/2"];
R2 -> T [color=red, penwidth=2, label="2/2"];
R3 -> T [color=red, penwidth=2, label="2/2"];
R4 -> T [color=red, penwidth=2, label="1/2"];
// 标注图例
subgraph cluster_legend {
label = "说明";
fontsize = 10;
rankdir = TB;
node [shape=plaintext];
legend1 [label="红色粗线: 成功匹配的路径"];
}
}
方案解释
在这个可能的解法中,共有7人被满足:
- Row 1 (满): 坐了 P2, P3
- Row 2 (满): 坐了 P1, P8
- Row 3 (满): 坐了 P4, P7
- Row 4 (半满): 坐了 P6
- 未满足: P5 (因为他想去的 Row 1 和 Row 3 都坐满了)
总计:
建模思路
我们将问题转化为网络流模型:
- 节点划分:
- 源点
:代表流量的起点。 - 人节点 (
):代表 个参赛者。 - 座位排行节点 (
):代表 排座位。 - 汇点
:代表匹配成功。
- 源点
- 建边策略:
- 源点
人: - 从
向每个“人节点”连一条边。 - 容量为 1:表示每个人只能被安排一个位置。
- 从
- 人
座位排: - 如果第
个人想坐第 排或第 排,则从“人节点 ”分别向“座位排节点 ”和“座位排节点 ”连边。 - 容量为 1:表示这个人如果去这排,占 1 个坑。
- 如果第
- 座位排
汇点: - 从每个“座位排节点”向
连一条边。 - 容量为 2:题目限制“每排座位只能坐两人”。
- 从每个“座位排节点”向
- 源点
- 答案:
- 该网络的最大流即为最多能满足的人数。
节点编号映射
为了在代码中方便处理,我们规定:
- 人节点:
- 座位排节点:
代码
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-11 22:34:24
* 题目: P2071 座位安排
* 算法: Dinic 最大流
* 模型: 二分图多重匹配
*/
#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; // s: 源点, t: 汇点
int a[maxn];
// --- 存图模板: 链式前向星 (Chain Forward Star) ---
// 这种结构在图论和网络流中非常常用,效率高,空间紧凑
struct linkList {
typedef struct {int u, v, w, next;} edge;
edge e[maxe]; // 边数组
int h[maxn], edge_cnt=0; // h[u]存储点u的第一条边索引
linkList(){
reset();
}
void reset() {
edge_cnt = 0;
memset(h, -1, sizeof(h)); // 初始化头指针为 -1
}
// 遍历点 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]; }
// --- C++ 语法糖 (Range-based for loop 支持) ---
// 这部分让遍历边可以写成 for(auto& edge : list(u)) 的形式
// 虽然下面的 Dinic 实现中主要用了传统的 for 循环,但这是很好的模板特性
#ifdef __cpp_range_based_for
struct UseEdge {
using ReturnType = edge&;
static ReturnType get(linkList* p, int i) { return p->e[i]; }
};
struct UseAdj {
using ReturnType = int;
static ReturnType get(linkList* p, int i) { return p->e[i].v; }
};
template<typename Getter>
struct BaseIterator {
int i;
linkList* p;
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; }
typename Getter::ReturnType operator*() { return Getter::get(p, i); }
};
using Iterator = BaseIterator<UseEdge>;
using AdjIterator = BaseIterator<UseAdj>;
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); }
};
BaseRange<Iterator> operator()(int u) { return BaseRange<Iterator>(this, u); }
BaseRange<AdjIterator> adj(int u) { return BaseRange<AdjIterator>(this, u); }
#endif
} e;
// --- Dinic 算法模板 ---
// 相比 EK 算法,Dinic 通过 BFS 分层和 DFS 多路增广,效率更高
struct Dinic {
vector<int> level, cur; // level: BFS分层深度, cur: 当前弧优化指针
int n; // 节点总数
// 初始化 Dinic 结构
void init(int n) {
// 重置链式前向星
e.edge_cnt = 0;
memset(e.h, -1, sizeof(e.h));
// 调整 vector 大小
level.resize(n+5);
cur.resize(n+5);
this->n = n;
}
// 添加网络流的边
// u->v 容量 cap, v->u 容量 0 (反向边)
void addEdge(int u, int v, int cap) {
e.add(u, v, cap); // 正向边,索引为偶数 (0, 2, 4...)
e.add(v, u, 0); // 反向边,索引为奇数 (1, 3, 5...)
}
// BFS 构建层次图 (Level Graph)
// 作用:确保增广路是“最短”的,防止绕圈,保证复杂度
bool bfs(int s, int t) {
fill(level.begin(), level.end(), -1); // 初始化所有点层级为 -1
queue<int> q;
level[s] = 0; // 源点层级为 0
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
// 遍历 u 的所有出边
for(int i = e.h[u]; ~i; i = e[i].next) {
int v = e[i].v, cap = e[i].w;
// 如果有残余容量 且 v 未访问过
if (cap > 0 && level[v] < 0) {
level[v] = level[u] + 1; // 标记层级
q.push(v);
}
}
}
return level[t] >= 0; // 如果能从 s 走到 t,返回 true
}
// DFS 寻找增广路 (多路增广)
// u: 当前点, t: 汇点, preFlow: 目前推过来的可用流量
long long dfs(int u, int t, long long preFlow) {
if (u == t || preFlow == 0) return preFlow; // 到达汇点或没流量了
long long flow = 0;
// --- 当前弧优化 (Current Arc Optimization) ---
// 这里的 &cid = cur[u] 非常关键
// 下次访问 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;
// 必须是分层图中下一层的点 (level[to] == level[u] + 1)
// 且必须有剩余容量
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; // 反向边容量增加 (异或 1 找到反向边)
flow += tr; // 累计流出的流量
preFlow -= tr; // 剩余可用流量减少
if (preFlow == 0) break; // 流量用尽,不需要再遍历后面的边了
}
// --- 炸点优化 ---
// 如果流不出去了,将 level 设为 -1,后续 BFS 不再访问该点
if (flow == 0) level[u] = -1;
return flow;
}
// 计算最大流的主函数
long long maxFlow(int s, int t) {
long long flow = 0;
while (bfs(s, t)) { // 只要还能分层 (即 s 能到达 t)
// 重置当前弧,让每个点从第一条边开始尝试
for (int i = 0; i <= n; i++) {
cur[i] = e.h[i];
}
// 进行多路增广
flow += dfs(s, t, LLONG_MAX);
}
return flow;
}
} dinic;
// --- 建图与初始化逻辑 ---
void init(){
std::cin >> n; // 读取 N (排数)
// 节点映射规划:
// 源点 s: 0
// 人节点: 1 到 2*n (共 2N 个人)
// 座位排节点: 2*n + 1 到 2*n + n (共 N 排)
// 汇点 t: 3*n + 1
s = 0;
t = 3*n + 1;
dinic.init(t + 5); // 初始化 Dinic 结构,分配空间
// 1. 处理 "人"
for (int i = 1; i <= 2*n; i++) {
// 源点 -> 人 (容量 1)
// 含义:每个人只能被安排一个座位
dinic.addEdge(s, i, 1);
int t1, t2; // 每个人想坐的两个排的编号
std::cin >> t1 >> t2;
// 人 -> 座位排 t1 (容量 1)
// 注意:座位排的节点编号需要加上偏移量 2*n
dinic.addEdge(i, 2*n + t1, 1);
// 人 -> 座位排 t2 (容量 1)
dinic.addEdge(i, 2*n + t2, 1);
}
// 2. 处理 "座位排"
for(int i = 1; i <= n; ++i)
{
// 座位排 -> 汇点 (容量 2)
// 含义:每排座位最多只能坐 2 个人
// 注意:座位排节点编号是 2*n + i
dinic.addEdge(2*n + i, t, 2);
}
}
// 主函数
int main() {
ios::sync_with_stdio(0); cin.tie(0); // 加速 IO
init(); // 读入数据并建图
// 输出最大流,即最多能满足多少人
cout << dinic.maxFlow(s,t) << "\n";
return 0;
}