【洛谷】P3386 【模板】二分图最大匹配

【洛谷】P3386 【模板】二分图最大匹配


洛谷专栏:模板,图论
洛谷模板:P3386 二分图最大匹配
算法竞赛:图论,二分图,二分图最大匹配
匹配算法:匈牙利算法
题目链接:洛谷 P3386

题目描述
给定一个二分图,其左部点的个数为 n n n,右部点的个数为 m m m,边数为 e e e,求其最大匹配的边数。
左部点从 1 1 1 n n n 编号,右部点从 1 1 1 m m m 编号。


二分图介绍

二分图是节点由两个集合组成,且两个集合内部没有边的图。即存在一种方案,将节点划分成满足以上性质的两个集合。

二分图相关基础知识可以参见 OI-WIKI

二分图匹配

在二分图中选出一些边作为一个集合,使任意两条边不存在公共点,称为二分图匹配。

在一个二分图中使选出的边数最大的匹配,称为二分图的最大匹配。

匈牙利算法

匈牙利算法是找二分图的最大匹配的经典算法。下面介绍匈牙利算法的步骤与思路和正确性证明。

算法步骤

  1. 我们通常将二分图存为一张有向图,通常存为从左部的点出发连向右部的点的边。
  2. 遍历每个左部顶点,并每次都将全部右部顶点设为未标记,(标记 1 1 1,见下条)在每个左部顶点中遍历其出边,如果其相连的右部顶点未被标记,则将其标记,判断其是否被其它左部顶点选中或能否取消被选中(见下条),如果没有被选中或能被取消选中,则被现左部顶点选中,并在最大匹配计数中加一。
  3. 递归函数——判断右部顶点能否被取消选中。函数步骤即从第二条(标记 1 1 1)处开始,判断右部顶点能否被取消选中即看和这个右部匹配的左部顶点能否与其他右部顶点匹配,让出现在这个右部顶点。

算法思路

从与左部顶点相连的右部顶点中选出能与左部顶点匹配的点,如果能相匹配则立刻选上,但比如有的左部顶点与两个右部顶点相连,第二个左部顶点只与其中一个右部顶点相连,最大匹配则为二,然而如果第一个左部顶点与两个共同所连的右部顶点相匹配,则匹配数就为一了,显然不正确,那么就需要在第二个左部点选右部点的时候看看第一个左部点能否选其他点,从而把公用相连的右部顶点让出来使第二个左部点与之匹配,这样使匹配数最大,如果不能让出来,便就此作罢。

正确性证明

  1. 模拟证明
  2. 增广路径证明(现无)
模拟算法过程

见下图。

1 7
1 5
3 7
3 2
4 8
4 2
6 5
6 2

  1. 从第一个点遍历,点 1 1 1 匹配到点 7 7 7
  2. 第二个点 3 3 3 要匹配点 7 7 7,点 1 1 1 可以重匹配点 5 5 5,则点 3 3 3 匹配点 7 7 7
  3. 4 4 4 匹配到点 2 2 2
  4. 6 6 6 要匹配点 5 5 5,点 5 5 5 被点 1 1 1 占着且不能被让出,则点 6 6 6 要匹配点 2 2 2,虽然点 2 2 2 被点 4 4 4 占着,但点 4 4 4 可以重匹配到点 8 8 8,让出点 2 2 2 使点 6 6 6 与之匹配。

则这张二分图最大匹配数为 4 4 4

我们可以把二分图匹配时遇到的所有情况列出来,分析每种情况是否可行即可。

情况一:

显然做法正确。

情况二:

根据上述过程模拟来看也是正确的。

情况三:(上述两种情况的组合体)

根据上述过程模拟来看这种情况下算法也是正确的,由这种情况可得知拆分或组合二分图不会影响算法正确性。

其余所有情况都可以转化、化简、拆分为上述前两种情况。

综上此做法正确。

下面给出完整 AC 代码。
#include <bits/stdc++.h>
using namespace std;
const int N = 510;
vector<int>G[N];
int march[N];
bool ins[N];//标记右部点
bool make(int x)
{
    for (int y: G[x])//遍历出边
    {
        if (!ins[y])
        {
            ins[y]=true;//标记
            if (march[y]==0||make(march[y]))//没有被标记或者能被让出来
            {
                march[y]=x;//右部点 y 是被左部点 x 标记的
                return true;
            }
        }
    }
    return false;
}
int main()
{
    int n1,n2,m,ans=0;
    scanf("%d%d%d",&n1,&n2,&m);
    for (int i=0;i<m;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        G[x].push_back(y);//存有向图
    }
    for (int i=1;i<=n1;i++)
    {
        memset(ins,0,sizeof ins);//取消标记
        if (make(i)) ans++;
    }
    printf("%d",ans);
    return 0;
}

End

感谢观看,如有问题欢迎指出。

更新日志

  1. 2025/8/19 开始书写本篇 CSDN 博客,并完稿发布。

本篇博客最早由本人发布于洛谷文章广场,本篇博客对其进行了修改调整与完善丰富。

### 关于洛谷 P1249 最大乘积问题的 C++ 解题思路 对于给定的一个正整数 `n` 和分割次数 `k`,目标是将 `n` 分割成 `k` 个部分使得这些部分的乘积最大化。这个问题可以通过动态规划来解决。 #### 动态规划的状态定义 设 `dp[i][j]` 表示前 `i` 位数字分成 `j` 段所能得到的最大乘积[^3]。 #### 初始化 - 对于只有一段的情况,即 `dp[i][1]` 就是从第1位到第i位组成的整个数值。 #### 状态转移方程 为了求解 `dp[i][j]` 的值,可以考虑最后一个切割位置 `p` (其中 `1 ≤ p < i`),则状态转移方程为: \[ dp[i][j]=\max(dp[p][j-1]*num(p+1,i)) \] 这里 `num(p+1,i)` 是指从第 `p+1` 到第 `i` 位所表示的子串对应的十进制数值。 #### 边界条件 当 `j=1` 或者 `i=j` 时,显然不需要进一步划分,因此可以直接赋初值;其他情况下通过上述公式计算得出结果。 下面是具体的代码实现: ```cpp #include<iostream> #include<string> using namespace std; const int MAX_N = 50; string s; long long f[MAX_N][MAX_N], num[MAX_N][MAX_N]; // 计算字符串s中从l到r之间的数字转换成long long型 void calc_num() { for (int l = 0; l < s.size(); ++l) for (int r = l; r < s.size(); ++r) { if (!l && !r) num[l][r] = s[l]-'0'; else num[l][r] = num[l][r-1]*10+s[r]-'0'; } } int main(){ int N, K; cin >> N >> K >> s; // 预处理每一段的数值 calc_num(); // 初始化边界情况 for(int i = 0; i<s.length();++i){ f[i+1][1]=num[0][i]; f[i+1][i+1]=f[i][i]*10+(s[i]-'0'); } // 填表过程 for(int j = 2;j<=K;++j)//枚举段数 for(int i = j;i<N;++i)//枚举终点 for(int k = j-1;k<i;++k)// 枚举上一次切分的位置 f[i][j]=max(f[i][j],f[k][j-1]*num[k][i]); cout << f[N-1][K]<<endl; } ``` 这段代码实现了基于动态规划的方法来寻找最优解,并且能够有效地处理高精度运算的需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HAH-HAH

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值