二分图最大匹配问题。
匈牙利好写一点,而且自带记录匹配对象。
但是既然练网络流就用网络流写吧。
建图:
源点连接左半部,汇点连接右半部,中间二分图,边权都为1。
在残余网络中找匹配对象:
利用前向星的成对变换遍历所有边和其反向边,如果当前遍历到的边不是与源点和汇点连接的边,则为二分图中间边,如果反向边边权不为0,即为匹配边(只有有流的边反向边不为0),该边的两端点就是一对答案。
ps:
题目没说有多少边,边数组要开大一点否则re
#include
using namespace std;
const int maxm=105+5;
const int inf=1e9;
int head[maxm],nt[maxm<<5],to[maxm<<5],w[maxm<<5],cnt;//题目没说一共有多少边,太坑了
int d[maxm];
int m,n;
int s,t;
int maxflow;
void init(){
memset(head,-1,sizeof head);
cnt=1;
}
void add(int x,int y,int z){
cnt++;nt[cnt]=head[x];head[x]=cnt;to[cnt]=y;w[cnt]=z;
}
bool bfs(){
memset(d,0,sizeof d);
queue<int>q;
q.push(s);
d[s]=1;
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x];i!=-1;i=nt[i]){
int v=to[i];
if(!d[v]&&w[i]){
d[v]=d[x]+1;
q.push(v);
if(v==t)return 1;
}
}
}
return 0;
}
int dfs(int x,int flow){
if(x==t)return flow;
int res=flow;
for(int i=head[x];i!=-1;i=nt[i]){
int v=to[i];
if(w[i]&&d[v]==d[x]+1){
int k=dfs(v,min(res,w[i]));
if(k){
w[i]-=k;
w[i^1]+=k;
res-=k;
}else{
d[v]=-1;
}
if(!res)break;
}
}
return flow-res;
}
void dinic(){
maxflow=0;
while(bfs()){
maxflow+=dfs(s,inf);
}
}
signed main(){
init();
scanf("%d%d",&m,&n);
s=0,t=n+1;
for(int i=1;i<=m;i++){//源点到外籍飞行员
add(s,i,1);
add(i,s,0);
}
for(int i=m+1;i<=n;i++){//英国飞行员到汇点
add(i,t,1);
add(t,i,0);
}
int a,b;
while(1){
scanf("%d%d",&a,&b);
if(a==-1&&b==-1)break;
add(a,b,1);//a到b
add(b,a,0);
}
dinic();
if(maxflow==0){//如果无解
puts("No solution");
}else{
printf("%d\n",maxflow);
for(int x=1;x<=m;x++){//遍历连接源点的点
for(int i=head[x];i!=-1;i=nt[i]){
if(w[i^1]){//如果反向边不为0说明是匹配边(有流量的边反向边才不为0)
if(to[i]==s||to[i]==t)continue;
printf("%d %d\n",x,to[i]);
break;
}
}
}
}
return 0;
}
//https://www.luogu.com.cn/problem/P2756
最大权闭合回路问题。
建图:
源点到实验,边权为收入
仪器到汇点,边权为成本
实验到实验所需的仪器,边权为inf
答案为实验收入和减去最小割。
求选择的实验和仪器:
最后残余网络中和源点连接的时选择的实验,和汇点连接的是选择的仪器。
正常来说可以dfs求解,但是我用dfs没跑出样例答案来,不知道为啥
看别人说用最后一次的层次数组d就可以求出答案:
如果d[i]为正值说明i点有流,所以判断实验和仪器的d[i]是否大于0就可以知道是否被选择
#include
using namespace std;
const int maxm=1e4+5;
const int inf=1e9;
int head[maxm],nt[maxm<<5],to[maxm<<5],w[maxm<<5],cnt;
int d[maxm];
int m,n;
int s,t;
int maxflow;
void init(){
memset(head,-1,sizeof head);
cnt=1;
}
void add(int x,int y,int z){
cnt++;nt[cnt]=head[x];head[x]=cnt;to[cnt]=y;w[cnt]=z;
}
bool bfs(){
memset(d,0,sizeof d);
queue<int>q;
q.push(s);
d[s]=1;
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x];i!=-1;i=nt[i]){
int v=to[i];
if(!d[v]&&w[i]){
d[v]=d[x]+1;
if(v==t)return 1;
q.push(v);
}
}
}
return 0;
}
int dfs(int x,int flow){
if(x==t)return flow;
int res=flow;
for(int i=head[x];i!=-1;i=nt[i]){
int v=to[i];
if(w[i]&&d[v]==d[x]+1){
int k=dfs(v,min(res,w[i]));
if(k){
w[i]-=k;
w[i^1]+=k;
res-=k;
}else{
d[v]=-1;
}
if(!res)break;
}
}
return flow-res;
}
void dinic(){
maxflow=0;
while(bfs()){
maxflow+=dfs(s,inf);
}
}
signed main(){
init();
scanf("%d%d",&m,&n);//m个实验,n个仪器
s=0,t=m+n+1;
int sum=0;//记录总收入
for(int i=1;i<=m;i++){
int get;//赞助商支付的费用,即收入
scanf("%d",&get);
sum+=get;
//
add(s,i,get);//源点到正权点
add(i,s,0);
//
char ch=getchar();
while(ch!='\n'&&ch!='\r'){//洛谷的这题输入方式不太一样
int x;//仪器编号
scanf("%d",&x);
//
add(i,m+x,inf);//实验到仪器的边权为inf
add(m+x,i,0);
//
ch=getchar();
}
}
for(int i=1;i<=n;i++){
int v;//仪器的成本,为负权
scanf("%d",&v);
//
add(m+i,t,v);//负权点到汇点
add(t,m+i,0);
//
}
dinic();
for(int i=1;i<=m;i++){
if(d[i]>0){//d[i]>0说明i被选择
printf("%d ",i);
}
}
puts("");
for(int i=1;i<=n;i++){
if(d[m+i]>0){
printf("%d ",i);
}
}
puts("");
printf("%d\n",sum-maxflow);
return 0;
}
//https://www.luogu.com.cn/problem/P2762
最小路径覆盖问题
建图:
对于每个点a,拆成入点a和出点aa
对于每条边x->y,建边yy->x (y的出点连x的入点)
源点连接每个出点,汇点连接每个出点,所有边权值为1
最少路径数:
最大流中每个流都表明两个点被合并了
所以点数n减去最大流maxflow就是剩下的点数,即需要的最少路径数
求路径:
我用的方法是遍历链式前向星中所有出点所在的边的反向边,
如果反向边的边权不为0,则说明有流,则为匹配边。
用数组pre记录每个点的前驱,link记录每个点的后继
对于每个匹配边x->y,pre[y]=x,link[x]=y
然后for循环从1到n找pre为0的点,则该点为路径起点,
用link数组从这个点开始不断输出路径,直到link为0,说明路径结束
具体操作可以看代码
#include
using namespace std;
const int maxm=1e4+5;
const int inf=1e9;
int head[maxm],nt[maxm<<3],to[maxm<<3],w[maxm<<3],cnt;
int pre[maxm];
int link[maxm];
int d[maxm];
int n,m;
int s,t;
int maxflow;
void init(){
memset(head,-1,sizeof head);
cnt=1;
}
void add(int x,int y,int z){
cnt++;nt[cnt]=head[x];head[x]=cnt;to[cnt]=y;w[cnt]=z;
}
bool bfs(){
memset(d,0,sizeof d);
queue<int>q;
q.push(s);
d[s]=1;
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x];i!=-1;i=nt[i]){
int v=to[i];
if(!d[v]&&w[i]){
d[v]=d[x]+1;
if(v==t)return 1;
q.push(v);
}
}
}
return 0;
}
int dfs(int x,int flow){
if(x==t)return flow;
int res=flow;
for(int i=head[x];i!=-1;i=nt[i]){
int v=to[i];
if(w[i]&&d[v]==d[x]+1){
int k=dfs(v,min(res,w[i]));
if(k){
w[i]-=k;
w[i^1]+=k;
res-=k;
}else{
d[v]=-1;
}
if(!res)break;
}
}
return flow-res;
}
void dinic(){
maxflow=0;
while(bfs()){
maxflow+=dfs(s,inf);
}
}
signed main(){
init();
scanf("%d%d",&n,&m);
s=0,t=n+n+1;
/*
源点0
汇点n+n+1
出点1-n
入点(n+1)-(n+n)
*/
for(int i=1;i<=n;i++){//源点到出点
add(s,i,1);
add(i,s,0);
}
for(int i=n+1;i<=n+n;i++){//入点到汇点
add(i,t,1);
add(t,i,0);
}
for(int i=1;i<=m;i++){
int a,b;
scanf("%d%d",&a,&b);
add(a,b+n,1);//a的出点连接b的入点
add(b+n,a,0);
}
dinic();
for(int x=1;x<=n;x++){//遍历出点
for(int i=head[x];i!=-1;i=nt[i]){
if(w[i^1]){//反向边不为0说明有流
if(to[i]==s||to[i]==t)continue;//跳过连接源点和汇点的
pre[to[i]-n]=x;//把对面端点的前驱设置为i
link[x]=to[i]-n;//当前点的后继设置为对面端点
}
}
}
for(int i=1;i<=n;i++){
if(!pre[i]){
int t=i;
while(t){
printf("%d ",t);
t=link[t];
}
puts("");
}
}
printf("%d\n",n-maxflow);
/*
最大流中的每个流都表明两个点被合并了
所以点数n减去最大流maxflow就是剩下的点数,即需要的最少路径数
*/
return 0;
}
//https://www.luogu.com.cn/problem/P2764
可以转化为最小路径覆盖问题
可以放在一起的球的方案看成点合并,最大流即为合并的点,就是最小路径覆盖
把每个球要拆成入点和出点,然后和为平方的建边(小编号的连向大编号)
球的个数减去最大流就是所需要的路径数,在这题里面也就是需要的柱子数
当柱子数超过的时候,去掉最后一次加的球,然后重新建图跑最大流就行了
需要注意的地方:
1.如果每次加一个球都重新建图会超时,所以每添加一个球在原图的基础上建图就行了
2.因为原图是跑过dinic的,所以是残余网络,在残余网络上加边然后再跑dinic之后,跑出来的最大流是流的增加量,所以再开一个变量记录总的流量
3.我路径的记录的方法和上面一篇的最小路径覆盖差不多,具体操作看代码
#include
using namespace std;
const int maxm=1e4+5;
const int inf=1e9;
int head[maxm],nt[maxm<<5],to[maxm<<5],w[maxm<<5],cnt;
int in[maxm],out[maxm],id[maxm];//
int pre[maxm],link[maxm];//
int d[maxm];
int n;
int s,t;
int maxflow;
void init(){
memset(head,-1,sizeof head);
cnt=1;
}
void add(int x,int y,int z){
cnt++;nt[cnt]=head[x];head[x]=cnt;to[cnt]=y;w[cnt]=z;
}
bool bfs(){
memset(d,0,sizeof d);
queue<int>q;
q.push(s);
d[s]=1;
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x];i!=-1;i=nt[i]){
int v=to[i];
if(!d[v]&&w[i]){
d[v]=d[x]+1;
if(v==t)return 1;
q.push(v);
}
}
}
return 0;
}
int dfs(int x,int flow){
if(x==t)return flow;
int res=flow;
for(int i=head[x];i!=-1;i=nt[i]){
int v=to[i];
if(w[i]&&d[v]==d[x]+1){
int k=dfs(v,min(res,w[i]));
if(k){
w[i]-=k;
w[i^1]+=k;
res-=k;
}else{
d[v]=-1;
}
if(!res)break;
}
}
return flow-res;
}
void dinic(){
maxflow=0;
while(bfs()){
maxflow+=dfs(s,inf);
}
}
signed main(){
init();
//
scanf("%d",&n);
int tot=0;
s=++tot;
t=++tot;
int now=0;
int flow=0;
while(now-flow<=n){
now++;
out[now]=++tot;id[tot]=now;//赋予编号,且记录编号对应的球的id
in[now]=++tot;id[tot]=now;
add(s,out[now],1);add(out[now],s,0);
add(in[now],t,1);add(t,in[now],0);
for(int i=1;i<now;i++){
int sum=i+now;
int sq=(int)sqrt(sum);
if(sq*sq==sum){
add(out[i],in[now],1);add(in[now],out[i],0);
}
}
dinic();
flow+=maxflow;
}
now--;//去掉不成立的最后一个球
init();//清空图
for(int i=1;i<=now;i++){//连接源点和汇点
add(s,out[i],1);add(out[i],s,0);
add(in[i],t,1);add(t,in[i],0);
}
for(int i=1;i<=now;i++){
for(int j=i+1;j<=now;j++){
int sum=i+j;
int sq=(int)sqrt(sum);
if(sq*sq==sum){
add(out[i],in[j],1);add(in[j],out[i],0);
}
}
}
dinic();
printf("%d\n",now);
for(int x=1;x<=now;x++){//枚举每个球
for(int i=head[out[x]];i!=-1;i=nt[i]){//遍历出点
if(w[i^1]){//如果反向边有流说明为匹配边
if(to[i]==s||to[i]==t)continue;
pre[id[to[i]]]=x;
link[x]=id[to[i]];
}
}
}
for(int i=1;i<=now;i++){
if(!pre[i]){
int t=i;
while(t){
printf("%d ",t);
t=link[t];
}
puts("");
}
}
return 0;
}
//https://www.luogu.com.cn/problem/P2765
二分图多重匹配问题
这题比较简单,建图如下:
源点连接每个单位,容量为单位代表人数,
每个圆桌连接汇点,容量为圆桌的容量,
每个单位连接每个圆桌,容量为1
建完图跑最大流就行了
#include
using namespace std;
const int maxm=1e4+5;
const int inf=1e9;
int head[maxm],nt[maxm<<5],to[maxm<<5],w[maxm<<5],cnt;
int d[maxm];
int m,n;
int s,t;
int maxflow;
void init(){
memset(head,-1,sizeof head);
cnt=1;
}
void add(int x,int y,int z){
cnt++;nt[cnt]=head[x];head[x]=cnt;to[cnt]=y;w[cnt]=z;
}
bool bfs(){
memset(d,0,sizeof d);
queue<int>q;
q.push(s);
d[s]=1;
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x];i!=-1;i=nt[i]){
int v=to[i];
if(!d[v]&&w[i]){
d[v]=d[x]+1;
if(v==t)return 1;
q.push(v);
}
}
}
return 0;
}
int dfs(int x,int flow){
if(x==t)return flow;
int res=flow;
for(int i=head[x];i!=-1;i=nt[i]){
int v=to[i];
if(w[i]&&d[v]==d[x]+1){
int k=dfs(v,min(res,w[i]));
if(k){
w[i]-=k;
w[i^1]+=k;
res-=k;
}else{
d[v]=-1;
}
if(!res)break;
}
}
return flow-res;
}
void dinic(){
maxflow=0;
while(bfs()){
maxflow+=dfs(s,inf);
}
}
signed main(){
init();
scanf("%d%d",&m,&n);
s=0,t=m+n+1;
int sum=0;//统计总人数
for(int i=1;i<=m;i++){
int x;
scanf("%d",&x);
sum+=x;
add(s,i,x);//源点连接单位,容量为代表人数
add(i,s,0);
}
for(int i=m+1;i<=m+n;i++){
int x;
scanf("%d",&x);
add(i,t,x);//餐桌连接汇点,容量为餐桌容量
add(t,i,0);
}
for(int i=1;i<=m;i++){
for(int j=m+1;j<=m+n;j++){
add(i,j,1);//每个单位对每个餐桌连边,容量为1
add(j,i,0);
}
}
dinic();
if(maxflow!=sum){//如果最大流不等于总人数说明无解
puts("0");
}else{//否则有解
puts("1");
for(int x=1;x<=m;x++){//遍历每个单位
for(int i=head[x];i!=-1;i=nt[i]){
if(w[i^1]){//反向边有流说明是匹配边
if(to[i]==s||to[i]==t)continue;//跳过和源点汇点连接的边
printf("%d ",to[i]-m);
}
}
puts("");
}
}
return 0;
}
//https://www.luogu.com.cn/problem/P3254