座位安排(洛谷-P2071)

题目背景

公元二零一四年四月十七日,小明参加了省赛,在一路上,他遇到了许多问题,请你帮他解决。

题目描述

已知车上有 N 排座位,有 2N 个人参加省赛,每排座位只能坐两人,且每个人都有两个自己想坐的排数,问最多使多少人坐到自己想坐的位置。

输入格式

第一行,一个正整数 N。

第二行至第 2N+1 行,每行两个正整数 Si,1​,Si,2​,为第 i 个人想坐的排数。

输出格式

一个非负整数,为最多使得多少人满意。

输入输出样例

输入 #1复制运行

复制代码
4
1 2
1 3
1 2
1 3
1 3
2 4
1 3
2 3

输出 #1复制运行

复制代码
7

说明/提示

对于 10% 的数据,n≤10;

对于 30% 的数据,n≤50;

对于 60% 的数据,n≤200;

对于 100% 的数据,n≤2000。

1. 题意转换

这道题本质上是在求:在一个左部节点数(人)为 2N、右部节点数(排数)为 N 且右部节点容量为 2 的二分图中,寻找最大匹配数。

2. 思考过程与解题思路

第一直觉的暴力解法是什么? 面对这种分配问题,初学者的第一直觉往往是 DFS 深度优先搜索,枚举每个人到底去他喜欢的两个座位中的哪一个。由于有 2N 个人,每个人有 2 种大方向选择,整体的时间复杂度会呈现指数级 O(2^2N)。当 N=2000 时,2^4000 的复杂度必定会导致严重的超时。

如何推导到正解? 我们将视角切换到图论模型:把"人"看作左边的点,把"座位"看作右边的点。每个人与他想坐的排数连一条边,问题就变成了经典的"匹配问题"。

但这里有一个极其棘手的冲突:传统的二分图匹配(如标准的匈牙利算法)是严格的"一对一"模型,也就是"一夫一妻制"。而这道题里,一排座位可以坐两个人,相当于右部的点"容量为 2"。 怎么办?

破局点:"拆点"思想。 既然算法只认"一对一",那我们就从物理层面把"容量为 2 的点"变成"两个容量为 1 的点"! 一共有 N 排座位,我们把第 i 排座位分裂成两个独立的座位:编号 i 和编号 n+i。 这样一来,右部节点的总数也变成了 2N(编号从 1 到 2N),每个座位刚好只能坐 1 个人。原题目瞬间降维,变成了一个完美的 2N×2N 的标准二分图最大匹配问题!

3. 算法设计与样例推演

核心算法与数据结构:

  • 匈牙利算法(DFS 实现):利用"协商让位(寻找增广路)"的核心逻辑来撮合匹配。

  • 拆点映射:左侧点为 1...2N(代表 2N 个人),右侧点为 1...2N(代表 2N 个独立座位)。第 x 排座位对应右侧的 x 和 x+n。

  • 邻接表 vector<int> vec:记录每个人可以坐的 4 个具体座位编号。

手玩推演核心逻辑(协商让位): 假设当前排到了第 u 个人,他想坐在第 1 排(拆分为座位 1 和座位 n+1)。

  1. 他去查看座位 1,如果单空着,直接坐下(匹配成功)。

  2. 假如座位 1 已经坐了张三,座位 n+1 坐了李四,怎么办?

  3. u 会对张三发出灵魂拷问(触发 dfs(linked[1])):"兄弟,你除了第 1 排,是不是还想坐第 3 排?你去第 3 排坐行不行,把第 1 排让给我?"

  4. 张三如果能在其他排找到空位,或者张三又能去把别人挤走......经过一条链式的"让位",只要最后有一个人找到了空位,整个链路上的所有人顺势交接,成功让 u 也坐下,总满意人数 +1。

4. 时空复杂度分析

  • 时间复杂度:O(V×E)。对于匈牙利算法,外层循环遍历左部点 V=2N。每次 DFS 最坏情况下遍历所有的边。由于每个人最多只有 2 个意向排数(拆点后连出 4 条边),所以边数 E=8N。总时间复杂度为 O(N^2)。当 N=2000 时,最大运算量在 4×10^6 左右,C++ 1秒内轻松跑完,非常合理。

  • 空间复杂度 :O(V+E)。邻接表存图空间为 8N 级别,used 和 linked 数组大小为 2N 级别。对于 N=2000,内存占用远不到 1MB,绝对不会 MLE。

5. 坑点与易错总结

  1. 数组越界(致命坑点) :题目给出的是 N,但实际的人数和拆分后的座位数都是 2N。如果习惯性地按照题目的变量 N 开数组(比如开 2050),必然会因为 4000 的下标访问导致 RE。务必将数组开到 4000 以上。

  2. 多条出边 :每次读取两个志愿排数 a 和 b,拆点后每个人必须连续 push_back 4个右部节点(a, n+a, b, n+b),少连任何一条边都会导致答案偏小。

6. 代码进阶与优化剖析(核心教学点)

审视一下我的代码,逻辑严密,"拆点"的思想落地得非常漂亮,这是一份可以AC的高质量代码!但为了让你在省选甚至更高阶的比赛中拥有更强的竞争力,我们需要探讨以下优化空间:

进阶优化 1:消除 memset 的常数开销(时间戳优化) 在我的 hungary() 函数中,每次处理一个新人物,都会执行一遍 memset(used, 0, sizeof(used));。虽然本题 2N=4000 不算大,但这就带来了 O(V^2) 的基础常数。 优化思路 :我们可以用"时间戳"替代布尔值。不要在每次循环都清空数组,而是把当前的轮次 u 当作标记传进去。判断条件从 !used[v] 改为 used[v] != u。这样,就能彻底抹除 memset,让算法常数呈数量级下降!

7.完整代码

cpp 复制代码
//很明显的二分匹配问题 求最大匹配数(即最多能让多少人满意)每匹配一对就等于是成功一排
//每一排等于是两个点 左边和右边
#include <iostream>
#include <vector>
#include <cstring>//对应memset
using namespace std;
int n;
int ln;//左边的点个数 即每个人
int rn;//右边的点个数 即排数(一共有n排)但每一排我们用两个数来表示
int used[4020];//代表右边的点在当前那一轮的循环中是否已经被使用
int linked[4020];//代表右边的座位所匹配的左边的人
vector<int> vec[4020];//邻接表存图

//x是本轮循环左边的点(人)
bool dfs(int x){
    //对于左边的每个点x(人),去遍历它能匹配到的(有线连接)的每个右点v
    //即对于左边的每个人(x)去遍历它能坐的所有右边的座位v
    for(int v:vec[x]){
        //如果右边的座位v这一轮还没有被坐
        //【优化】:可将此处改为if(used[v]!=x),配合外层传入时间戳x 即可彻底省略外层的memset
        if(!used[v]){
            //标记这一个座位已经被坐了
            used[v]=1;
            //如果v点之前没有匹配的左点 (座位空着) 
            //或v点之前匹配的左点可以找到新的匹配的右点从而把v释放出来 (强迫原主人换座)
            if(linked[v]==-1 || dfs(linked[v])){
                //就让v匹配的左点变成x(上位接手座位)
                linked[v]=x;
                //并返回成功匹配
                return true;
            }
        } 
    }
    //如果一直到这里都没有成功匹配返回false (没空位,别人也不肯让)
    return false;
}


int hungary(){
    int idx=0;//idx代表最多使得多少人满意
    //初始化所有座位都没有匹配人(值为-1)
    memset(linked,-1,sizeof(linked));
    //遍历所有左边的人
    for(int u=1;u<=2*n;u++){
        //每一轮循环初始化右边的点(座位)都没有被使用过
        //【优化】:若dfs使用时间戳判断,这里的memset就完全不需要了,极大降低运行常数
        memset(used,0,sizeof(used));
        if(dfs(u)) idx++;
    }
    return idx;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;
    ln=2*n;//左边点数为2n(2n个人)
    //右边点数为2n
    //(因为每一排有两个座位,所以第一排两个座位可以设置为1 n+1)
    //第二排两个座位2 n+2;第三排两个座位3 n+3......以此类推
    rn=2*n;
    //邻接表存图-边(每个人想坐的座位)
    for(int i=1;i<=2*n;i++){
        int a,b;
        //第i个人想坐第a排和第b排
        cin>>a>>b;
        //第i个人想坐第a排,第a排为a和n+a
        vec[i].push_back(a);
        vec[i].push_back(n+a);
        //第i个人想坐第b排,第b排为b和n+b
        vec[i].push_back(b);
        vec[i].push_back(n+b);
    }
    //匈牙利算法
    cout<<hungary();
    return 0;
}
相关推荐
巴糖1 小时前
挑礼物这件事,暴露了你做选择时最大的问题
算法
用户7341681035481 小时前
控制与监测特征的联合表征:复杂动力装备变工况故障诊断的发展方向
算法
垆边人似月.1 小时前
华为题:无重复字符的最长子串变种
算法
IOT那些事儿1 小时前
C++17和20版本差异
c++·mingw·msvc
糖果豆豆1 小时前
灾难性遗忘不是"新旧打架"——旧知识记不住,是因为它没长成"环"
算法
垆边人似月.1 小时前
华为题:任务调度
数据结构·c++·算法
垆边人似月.1 小时前
华为题:编辑距离
算法·华为
会灭火的程序员1 小时前
大模型部署硬件资源计算指南:从显存估算到显卡选型
人工智能·算法
信奥卷王1 小时前
[GESP202506 六级] 最大因数(含视频)
数据结构·算法