牛子图论进阶
二分图及最大匹配
NC20483 假期的宿舍
题意
学校放假了······有些同学回家了,而有些同学则有以前的好朋友来探访,那么住宿就是一个问题。比如A和B都是学校的学生,A要回家,而C来看B,C与A不认识。我们假设每个人只能睡和自己直接认识的人的床。那么一个解决方案就是B睡A的床而C睡B的床。
而实际情况可能非常复杂,有的人可能认识好多在校学生,在校学生之间也不一定都互相认识。我们已知一共有n个人,并且知道其中每个人是不是本校学生,也知道每个本校学生是否回家。
问是否存在一个方案使得所有不回家的本校学生和来看他们的其他人都有地方住。
思路
即将点分为所有需要床的学生和床,看最大匹配是否等于所有需要床的学生数即可
- 对于本校住校生,其需要床,并且可以匹配认识的本校住校生的床和自己的床
- 对于外校生,其可以匹配认识的本校住校生的床
代码
#include<bits/stdc++.h>#define ull unsigned long long
#define ll long long
#define inf 1e9
#define INF 1e18
#define lc p<<1
#define rc p<<1|1
#define endl '\n'
#define all(a) a.begin()+1,a.end()
#define all0(a) a.begin(),a.end()
#define lowbit(a) (a&-a)
#define fi first
#define se second
#define pb push_back
#define yes cout<<"YES"<<endl
#define no cout<<"NO"<<endlusing namespace std;
const double eps=1e-6;
typedef pair<int,int>PII;
typedef array<int,3>PIII;
mt19937_64 rnd(time(0)); const int N=100;
vector<int>e[N];void solve()
{int n;cin>>n;for(int i=1;i<=n;i++) e[i].clear();vector<int>school(n+1),home(n+1);for(int i=1;i<=n;i++) cin>>school[i];for(int i=1;i<=n;i++) cin>>home[i];int cnt=0;for(int i=1;i<=n;i++){if(!school[i]) cnt++;else if(!home[i]) cnt++;}vector<vector<int>>a(n+1,vector<int>(n+1));for(int i=1;i<=n;i++){for(int j=1;j<=n;j++) cin>>a[i][j];}for(int i=1;i<=n;i++){if(school[i] && !home[i]) e[i].pb(i);for(int j=1;j<=n;j++){if(j==i) continue;if(school[i] && !home[i] && a[i][j] && school[j]) e[i].pb(j);if(!school[i] && a[i][j] && school[j]) e[i].pb(j);}}int ans=0;vector<int>link(n+1);vector<int>vis(n+1);auto dfs=[&](auto && dfs,int u)->bool{for(auto ed:e[u]){if(vis[ed]) continue;vis[ed]=1;if(!link[ed] || dfs(dfs,link[ed])){link[ed]=u;return true;}}return false;};for(int i=1;i<=n;i++){fill(all(vis),0);if(dfs(dfs,i)) ans++;}if(cnt==ans) cout<<"^_^"<<endl;else cout<<"T_T"<<endl;
}int main()
{ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);int t;cin>>t;while(t--)solve();return 0;
}
NC 51316
题意
一张平面图上有若干小人和房子,小人和房子的数量相等,小人可以上下左右移动,代价为1金币,求所有小人都有房子的最小代价
思路
每个小人到房子的代价即其的曼哈顿距离,问题转化为小人和房子两两匹配的最小权,我们将边权取反后就变成了二分图最大权完美匹配模板
代码
#include<bits/stdc++.h>#define ull unsigned long long
#define ll long long
#define inf 1e9
#define INF 1e18
#define lc p<<1
#define rc p<<1|1
#define endl '\n'
#define all(a) a.begin()+1,a.end()
#define all0(a) a.begin(),a.end()
#define lowbit(a) (a&-a)
#define fi first
#define se second
#define pb push_back
#define yes cout<<"YES"<<endl
#define no cout<<"NO"<<endlusing namespace std;
const double eps=1e-6;
typedef pair<int,int>PII;
typedef array<int,3>PIII;
mt19937_64 rnd(time(0)); const int N=110;
int match[N];
int va[N],vb[N];
ll w[N][N];
ll la[N],lb[N];
ll d[N];int cnth,cntm;bool dfs(int x)
{va[x]=1;for(int y=1;y<=cnth;y++){if(!vb[y]){if(la[x]+lb[y]==w[x][y]){vb[y]=1;if(!match[y] || dfs(match[y])){match[y]=x;return true;}}else d[y]=min(d[y],la[x]+lb[y]-w[x][y]);}}return false;
}ll km()
{for(int i=1;i<=cnth;i++) la[i]=-INF;for(int i=1;i<=cntm;i++){for(int j=1;j<=cnth;j++) la[i]=max(la[i],w[i][j]);}for(int i=1;i<=cnth;i++) lb[i]=0;for(int i=1;i<=cntm;i++){while(1){fill(va+1,va+1+cntm,0);fill(vb+1,vb+1+cnth,0);fill(d+1,d+cnth+1,INF);if(dfs(i)) break;ll delta=INF;for(int j=1;j<=cnth;j++) if(!vb[j]) delta=min(delta,d[j]);for(int j=1;j<=cnth;j++){if(va[j]) la[j]-=delta;if(vb[j]) lb[j]+=delta;}}}ll res=0;for(int i=1;i<=cnth;i++) res+=w[match[i]][i];return res;
}void solve()
{int n,m;while(cin>>n>>m,n && m){memset(match,0,sizeof match);vector<vector<char>>a(n+1,vector<char>(m+1));vector<PII>house,human;for(int i=1;i<=n;i++){for(int j=1;j<=m;j++){cin>>a[i][j];if(a[i][j]=='H') human.pb({i,j});else if(a[i][j]=='m') house.pb({i,j});}}cntm=human.size();cnth=house.size();for(int i=1;i<=cntm;i++){for(int j=1;j<=cnth;j++) w[i][j]=INF;}for(int i=0;i<cntm;i++){for(int j=0;j<cnth;j++){auto [x,y]=human[i];auto [a,b]=house[j];w[i+1][j+1]=-(abs(x-a)+abs(y-b));}}ll ans=km();cout<<-ans<<endl;}
}int main()
{ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);solve();return 0;
}
POJ3041
题意
给定一个n∗nn*nn∗n的矩阵,以及k个陨石的坐标,每次可以选择击碎一行或一列的陨石,求击碎所有陨石的最小操作次数
思路
如果将每个陨石坐标的行向列连边,那么对于每一个行顶点,每条出边都代表一颗陨石,那么现在问题转化成每次可以选择一个顶点的全部出边,求选择最少顶点覆盖所有边的顶点数,毫无疑问是最小点覆盖问题,最小点覆盖问题又等价于最大边匹配
代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define ll long long
#define inf 1e9
#define INF 1e18
#define lc p<<1
#define rc p<<1|1
#define endl '\n'
#define all(a) a.begin()+1,a.end()
#define all0(a) a.begin(),a.end()
#define lowbit(a) (a&-a)
#define fi first
#define se second
#define pb push_back
#define yes cout<<"YES"<<endl
#define no cout<<"NO"<<endlusing namespace std;
const double eps=1e-6;
typedef pair<int,int>PII;
typedef array<int,3>PIII;
mt19937_64 rnd(time(0)); const int N=510;
vector<int>e[N];void solve()
{int n,k;cin>>n>>k;for(int i=1;i<=k;i++){int a,b;cin>>a>>b;e[a].pb(b);}ll ans=0;vector<int>vis(n+1),match(n+1);auto dfs=[&](auto &&dfs,int u)->bool{for(auto ed:e[u]){if(vis[ed]) continue;vis[ed]=1;if(!match[ed] || dfs(dfs,match[ed])){match[ed]=u;return true;}}return false;};for(int i=1;i<=n;i++){fill(all(vis),0);if(dfs(dfs,i)) ans++;}cout<<ans<<endl;}int main()
{ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);solve();return 0;
}
P1129矩阵游戏
题意
小 Q 是一个非常聪明的孩子,除了国际象棋,他还很喜欢玩一个电脑益智游戏――矩阵游戏。矩阵游戏在一个 n×nn \times nn×n 黑白方阵进行(如同国际象棋一般,只是颜色是随意的)。每次可以对该矩阵进行两种操作:
- 行交换操作:选择矩阵的任意两行,交换这两行(即交换对应格子的颜色)。
- 列交换操作:选择矩阵的任意两列,交换这两列(即交换对应格子的颜色)。
游戏的目标,即通过若干次操作,使得方阵的主对角线(左上角到右下角的连线)上的格子均为黑色。
对于某些关卡,小 Q 百思不得其解,以致他开始怀疑这些关卡是不是根本就是无解的!于是小 Q 决定写一个程序来判断这些关卡是否有解。
第一行包含一个整数 TTT,表示数据的组数,对于每组数据,输入格式如下:
第一行为一个整数,代表方阵的大小 nnn。
接下来 nnn 行,每行 nnn 个非零即一的整数,代表该方阵。其中 000 表示白色,111 表示黑色。
对于每组数据,输出一行一个字符串,若关卡有解则输出 Yes
,否则输出 No
。
- 对于 20%20\%20% 的数据,保证 n≤7n \leq 7n≤7。
- 对于 50%50\%50% 的数据,保证 n≤50n \leq 50n≤50。
- 对于 100%100\%100% 的数据,保证 1≤n≤2001 \leq n \leq 2001≤n≤200,1≤T≤201 \leq T \leq 201≤T≤20。
思路
可以发现对于同一行同一列的点,交换之后仍然同行同列,所有问题转化为能否找到n个不同行同列的点(这样一定能转化为主对角线上),考虑对于黑色格子的行向列连边,问题又转化为二分图的最大匹配
代码
#include<bits/stdc++.h>#define ull unsigned long long
#define ll long long
#define inf 1e9
#define INF 1e18
#define lc p<<1
#define rc p<<1|1
#define endl '\n'
#define all(a) a.begin()+1,a.end()
#define all0(a) a.begin(),a.end()
#define lowbit(a) (a&-a)
#define fi first
#define se second
#define pb push_back
#define yes cout<<"Yes"<<endl
#define no cout<<"No"<<endlusing namespace std;
const double eps=1e-6;
typedef pair<int,int>PII;
typedef array<int,3>PIII;
mt19937_64 rnd(time(0)); const int N=210;
vector<int>e[N];void solve()
{int n;cin>>n;for(int i=1;i<=n;i++) e[i].clear();vector<vector<int>>a(n+1,vector<int>(n+1));for(int i=1;i<=n;i++){for(int j=1;j<=n;j++){cin>>a[i][j];if(a[i][j]) e[i].pb(j);} }vector<int>match(n+1),vis(n+1);auto dfs=[&](auto &&dfs,int u)->bool{for(auto ed:e[u]){if(vis[ed]) continue;vis[ed]=1;if(!match[ed] || dfs(dfs,match[ed])){match[ed]=u;return true;}}return false;};int ans=0;for(int i=1;i<=n;i++){fill(all(vis),0);if(dfs(dfs,i)) ans++;}if(ans==n) yes;else no;}int main()
{ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);int t;cin>>t;while(t--)solve();return 0;
}
NC236758 占领城市
题意
n个点m条道路,选择最少的道路覆盖全部的点,不可以重复经过点
思路
最少不重复路径覆盖板子题,对于互相可达的两个点连一条边,那么每次选择一条边可以覆盖两个城市,答案即为n-最大匹配
代码
#include<bits/stdc++.h>#define ull unsigned long long
#define ll long long
#define inf 1e9
#define INF 1e18
#define lc p<<1
#define rc p<<1|1
#define endl '\n'
#define all(a) a.begin()+1,a.end()
#define all0(a) a.begin(),a.end()
#define lowbit(a) (a&-a)
#define fi first
#define se second
#define pb push_back
#define yes cout<<"YES"<<endl
#define no cout<<"NO"<<endlusing namespace std;
const double eps=1e-6;
typedef pair<int,int>PII;
typedef array<int,3>PIII;
mt19937_64 rnd(time(0)); const int N=510;vector<int>e[N];void solve()
{int n,m;cin>>n>>m;for(int i=1;i<=m;i++){int a,b;cin>>a>>b;e[a].pb(b);}int ans=0;vector<int>vis(n+1),match(n+1);auto dfs=[&](auto && dfs,int u)->bool{for(auto ed:e[u]){if(vis[ed]) continue;vis[ed]=1;if(!match[ed] || dfs(dfs,match[ed])){match[ed]=u;return true;}}return false;};for(int i=1;i<=n;i++){fill(all(vis),0);if(dfs(dfs,i)) ans++;}cout<<n-ans<<endl;
}int main()
{ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);solve();return 0;
}
NC 236759中心图
题意
给出一个 nnn 个节点, mmm 条边的有向图,你可以进行若干次操作,每次操作可以删除图中的一条边或者添加一条有向边,使得原图变为中心图。
定义一个图是中心图,当且仅当它满足如下条件:
1. 图中没有重边。
2. 存在一个点uuu,满足对于任意的 v∈[1,n]v \in [1,n]v∈[1,n] ,图中都存在边 (u,v)(u,v)(u,v) 和 (v,u)(v,u)(v,u) ,我们称 uuu 是中心点。注意:这意味着 uuu 需要有自环。
3. 除了中心点之外,其它所有点的入度和出度均为2。
此外,你需要保证操作次数尽可能少。
第一行输入两个整数 n,m(2≤n≤500,1≤m≤1000)n,m(2 \le n \le 500,1 \le m \le 1000)n,m(2≤n≤500,1≤m≤1000) ,分别表示原图的点数和边数。
接下来mmm行,每行输入两个整数 ui,vi(1≤ui,vi≤n)u_i,v_i(1\le u_i,v_i \le n)ui,vi(1≤ui,vi≤n) ,表示从 uiu_iui 到 viv_ivi 有一条有向边相连。保证输入不含重边。
思路
对于中心点u可以枚举,对于每个中心点计算答案
- 对于条件2,记录没有和中心点u连边的边数,记为cnt
- 对于条件3,去掉和中心点u的边之后,等价于n-1个点都需要有一条出边入边,对这n-1个点跑最大匹配即可,那么对于孤立点只需要连向自己即可
- 那么需要删除的边为m-(和中心点连接的边+最大匹配)
- 需要添加的边为n-1-最大匹配+没有和中心点u连接的边
代码
#include<bits/stdc++.h>#define ull unsigned long long
#define ll long long
#define inf 1e9
#define INF 1e18
#define lc p<<1
#define rc p<<1|1
#define endl '\n'
#define all(a) a.begin()+1,a.end()
#define all0(a) a.begin(),a.end()
#define lowbit(a) (a&-a)
#define fi first
#define se second
#define pb push_back
#define yes cout<<"YES"<<endl
#define no cout<<"NO"<<endl
#define i128 __int128using namespace std;
const double eps=1e-6;
typedef pair<int,int>PII;
typedef array<int,3>PIII;
mt19937_64 rnd(time(0)); const int N=510;int mp[N][N];void solve()
{int n,m;cin>>n>>m;for(int i=1;i<=m;i++){int a,b;cin>>a>>b;mp[a][b]++;}ll ans=1e9;auto work=[&](int x){int cnt=0;//缺少的边for(int i=1;i<=n;i++){if(i==x){if(!mp[i][x]) cnt++;continue;}if(!mp[i][x]) cnt++;if(!mp[x][i]) cnt++;}int res=0;//最大匹配,有用的边vector<int>match(n+1),vis(n+1);auto dfs=[&](auto &&dfs,int x,int v)->bool{for(int i=1;i<=n;i++){if(i==v) continue;if(vis[i] || !mp[i][x]) continue;vis[i]=1;if(!match[i] || dfs(dfs,match[i],v)){match[i]=x;return true;}}return false;};for(int i=1;i<=n;i++){if(i==x) continue;fill(all(vis),0);if(dfs(dfs,i,x)) res++;}//一共有n-1个点需要和中心点连两条边,那么有2*n-1-cnt条有用的//m-有用-res 给出的边中没用的,既需要删除的ll del=m-(2*n-1-cnt)-res;//n-1-res n-1个点需要一出一入,对于不在最大匹配里的和自己连边即可ll add=n-1-res;return del+add+cnt;};for(int i=1;i<=n;i++){ans=min(ans,work(i));}cout<<ans<<endl;
}int main()
{ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);solve();return 0;
}
NC 236773插座
题意
Playf刚刚搬完家,他的新家有 mmm 个插座,编号依次为 1,2,...,m1,2,...,m1,2,...,m 。Playf总共有 nnn 个电器,编号依次为 1,2,...,n1,2,...,n1,2,...,n。 出于某些原因,每一个电器只能与特定的插座连接,每个插座只能连接一个电器。具体地,总共有 kkk 种电器和插座的连接方式。此外,Playf还带了一个插线板,这意味着Playf可以把插线板连在某一个插座上,使得这个插座最多能连接3个电器。Playf想知道他最多能让多少个电器成功连到适合的插座上。
第一行输入三个整数 m,n,k(1≤n,m≤1500,0≤k≤75000)m,n,k(1\le n,m \le 1500, 0\le k \le 75000)m,n,k(1≤n,m≤1500,0≤k≤75000) ,分别表示插座个数,电器个数,连接方式总数。
接下来 kkk行,每行两个整数 ui,vi(1≤ui≤m,1≤vi≤n)u_i,v_i(1\le u_i \le m,1 \le v_i \le n)ui,vi(1≤ui≤m,1≤vi≤n) ,描述一个连接方式,表示编号为 uiu_iui 的插座可以被编号为 viv_ivi 的电器连接。
思路
回忆二分图最大匹配的算法流程,对每个点不断找增广路,存在增广路则匹配数加1,因此我们可以求出原图的最大匹配之后尝试对每个点进行扩展,建立两个虚点,其连边方式和尝试扩展的点连边方式相同,之后对这两个点找增广路,注意每个点扩展完之后需要恢复到原图的状态
代码
#include<bits/stdc++.h>#define ull unsigned long long
#define ll long long
#define inf 1e9
#define INF 1e18
#define lc p<<1
#define rc p<<1|1
#define endl '\n'
#define all(a) a.begin()+1,a.end()
#define all0(a) a.begin(),a.end()
#define lowbit(a) (a&-a)
#define fi first
#define se second
#define pb push_back
#define yes cout<<"YES"<<endl
#define no cout<<"NO"<<endl
#define i128 __int128using namespace std;
const double eps=1e-6;
typedef pair<int,int>PII;
typedef array<int,3>PIII;
mt19937_64 rnd(time(0)); const int N=1510;
vector<int>e[N<<3];void solve()
{ll n,m,k;cin>>n>>m>>k;for(int i=1;i<=k;i++){int a,b;cin>>a>>b;e[a].pb(b);}int cnt=0;vector<int>vis(m+10);vector<int>match(m+10);auto dfs=[&](auto && dfs,int u)->bool{for(auto ed:e[u]){if(vis[ed]) continue;vis[ed]=1;if(!match[ed] || dfs(dfs,match[ed])){match[ed]=u;return true;}}return false;};for(int i=1;i<=n;i++){fill(all(vis),0);if(dfs(dfs,i)) cnt++;}//cout<<cnt<<endl;int res=0;for(int i=1;i<=n;i++){auto match1=match;auto e1=e;int add=0;for(auto ed:e[i]){e1[i+n].pb(ed);e1[i+2*n].pb(ed);}auto dfs1=[&](auto &&dfs1,int u)->bool{for(auto ed:e1[u]){if(vis[ed]) continue;vis[ed]=1;if(!match1[ed] || dfs1(dfs1,match1[ed])){match1[ed]=u;return true;}}return false;};fill(all(vis),0);if(dfs1(dfs1,i+n)) add++;fill(all(vis),0);if(dfs1(dfs1,i+2*n)) add++;res=max(res,add);}cout<<cnt+res<<endl;
}int main()
{ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);solve();return 0;
}
NC34649 color
题意
给一个没有重边的二分图,要求给边染色,有公共点的边不能同色,求最少颜色数以及染色方案
思路
最少颜色数一定是度数最多的点,每次从度数最多的点染色,和寻找增广路类似
代码
#include<bits/stdc++.h>#define ull unsigned long long
#define ll long long
#define inf 1e9
#define INF 1e18
#define lc p<<1
#define rc p<<1|1
#define endl '\n'
#define all(a) a.begin()+1,a.end()
#define all0(a) a.begin(),a.end()
#define lowbit(a) (a&-a)
#define fi first
#define se second
#define pb push_back
#define yes cout<<"YES"<<endl
#define no cout<<"NO"<<endl
#define i128 __int128using namespace std;
const double eps=1e-6;
typedef pair<int,int>PII;
typedef array<int,3>PIII;
mt19937_64 rnd(time(0)); const int N=1010;
vector<int>e[N];void solve()
{int n,m;cin>>n>>m;vector<int>x(m+1),y(m+1);vector<int>d(n+1);int mx=-1e9;for(int i=1;i<=m;i++){cin>>x[i]>>y[i];d[x[i]]++;d[y[i]]++;mx=max({mx,d[x[i]],d[y[i]]});}auto cmp=[&](int x,int y) {return d[x]>d[y];};vector<int>match(n+1),vis(n+1);vector<int>id(n+1);vector<vector<int>>color(n+1,vector<int>(n+1));for(int i=1;i<=n;i++) id[i]=i;auto dfs=[&](auto && dfs,int u)->bool{for(auto ed:e[u]){if(vis[ed]) continue;vis[ed]=1;if(!match[ed] || dfs(dfs,match[ed])){match[ed]=u;match[u]=ed;return true;}}return false;};for(int i=1;i<=mx;i++){for(int j=1;j<=m;j++){if(!color[x[j]][y[j]]){e[x[j]].pb(y[j]);e[y[j]].pb(x[j]);}}sort(all(id),cmp);fill(all(match),0);for(int p=1,k=id[p];p<=n;k=id[++p]){if(!match[k]){fill(all(vis),0);dfs(dfs,k);}}for(int j=1;j<=n;j++){if(match[j]){color[j][match[j]]=i;//cout<<j<<" "<<match[i]<<i<<endl;d[j]--;}e[j].clear();}}cout<<mx<<endl;for(int i=1;i<=m;i++){cout<<color[x[i]][y[i]]<<endl;}
}int main()
{ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);solve();return 0;
}