二分图总结

1.二分图判定:
一个二分图就是两个点集,也就是不能含有奇数环。例题:图的遍历
2.最大匹配:
可以建立单向边,直接n次dfs,求出最大匹配即可。例题:飞行员
建立双向边,n次dfs,求出最大匹配除以2即可。例题:Cat VS Dog
3.最大独立集:
求出最大的集合,里面的任意两点间没有边。
最大独立集 = n-最大匹配。例题:Cat VS Dog
4.最小点覆盖:
就是用最少的点,把每条边都覆盖掉。
最小点覆盖 = 最大匹配。例题:机器任务
5.最小边覆盖:
就是用最少的边,把每个点都覆盖掉。
最小边覆盖 = n-最大匹配。例题:Antenna Placement
6.最小不重复路径覆盖:
就是让你用最少的不相交的路径去覆盖整个有向无环图。看到了,要求的是有向无环的,所以并不一定就是二分图,但是求最大匹配需要二分图啊,所以一般就是把一个点a拆成a1和a2,如果a和b有一条边,那么就加入a1和b2这条边。对新图求一遍最大匹配即可。一开始每个点都是独立的为一条路径,总共有n条不相交路径。我们每次在二分图里找一条匹配边就相当于把两条路径合成了一条路径,也就相当于路径数减少了1。所以找到了几条匹配边,路径数就减少了多少。所以有最小路径覆盖=原图的结点数-新图的最大匹配数。因为路径之间不能有公共点,所以加的边之间也不能有公共点,这就是匹配的定义。其实呢只要保证加的边之间没有公共点,那么所谓的新图其实就是题目给的图。如果不能保证的话,就要把b点加一个定值了。注意新图一定要是有向图,不能是无向图。
最小不重复路径覆盖 = n-新图的最大匹配。例题:Air Raid
7.最小重复路径覆盖:
就是让你用最少的可以相交的路径去覆盖整个图,这个呢一般点都会比较少,所以用邻接矩阵存图,由于是可重复的,所以要先floyd传递闭包,然后再求最大匹配即可。这样就转化成了不重复的匹配了。
最小重复路径覆盖 = n-传递闭包后的最大匹配。例题:Treasure Exploration
8.最大团:
最大团就是一个集合内,任意两点都有连边。
最大团 = 补图的最大独立 = n-补图的最大匹配。例题:Kindergarten
9.最大权匹配:
就是每条边现在有了权值,问你最大的匹配权值和是多少。
先把题意转化为矩阵图,然后直接KM板子跑一遍就行了。例题:Spy

图的遍历:
题意:就是给你一个图,每次只能跳着走,也就是一下要走两步,问你最少要加多少边可以从一个点走完整张图。
思考:既然跳着走,如果有一个奇数环,那么就可以让步数少1那么可以遍历整个图,如果没有奇数环的话加一条边构造出就行了,还有就是图不一定联通,所以还要加上联通的边数。
代码:

图的遍历代码:
int T,n,m,k;
int va[N];
int col[N];
int suc;

vector<int > e[N];

void dfs(int now,int p)
{
	for(auto spot:e[now])
	{
		if(spot==p) continue;
		if(!col[spot])
		{
			col[spot] = 3-col[now];
			dfs(spot,now);
		}
		else if(col[spot]==col[now]) suc = 1;
	}
}

signed main()
{
	IOS;
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	{
		int a,b;
		cin>>a>>b;
		e[a].pb(b);
		e[b].pb(a);
	}
	int ans = 0;
	for(int i=1;i<=n;i++)
	{
		if(!col[i])
		{
			ans++;
			col[i] = 1;
			dfs(i,0);
		}
	}
	cout<<ans-1+(suc==0);
	return 0;
}

飞行员:
题意:就是有n个皇家飞行员,m个外国飞行员,每个外国飞行员可以配对某些皇家飞行员,现在让你求出最大的匹配数是多少。
思考:就是直接最大匹配就行了,而且是很明显的两个点集。

飞行员代码:
int T,n,m,k;
int va[N];
int vis[N],match[N];
int anw[N];

vector<int > e[N];

bool dfs(int now)
{
	for(auto spot:e[now])
	{
		if(vis[spot]) continue;
		vis[spot] = 1;
		if(match[spot]==0||dfs(match[spot]))
		{
			match[spot] = now;
			anw[now] = spot;
			return true;
		}
	}
	return false;
}

signed main()
{
	IOS;
	cin>>m>>n;
	while(1)
	{
		int a,b;
		cin>>a>>b;
		if(a==-1&&b==-1) break;
		e[a].pb(b); //如果这里建立了双向边
	}
	int ans = 0;
	for(int i=1;i<=n;i++) //这里也是n,那么最大匹配就会多一倍,最后要除以2
	{
		for(int j=1;j<=n;j++) vis[j] = 0;
		if(dfs(i)) ans++;
	}
	cout<<ans<<"\n";
	for(int i=1;i<=m;i++)
	{
		if(anw[i]) cout<<i<<" "<<anw[i]<<"\n";
	}
	return 0;
}

Cat VS Dog:
题意:就是有k个小朋友,每个人喜欢一个动物,也会讨厌一个动物,然后他们去动物园玩,如果一个孩子喜欢的动物在公园里面,他讨厌的不在,那么他就会开心。现在动物园会移走一些动物,问你最多可以让多少小朋友开心。
思考:就是很明显的选择最多的小孩,他们之间不会有矛盾,也就是求最大独立集。有矛盾也就是A喜欢的动物是B讨厌的。但是呢,这个点集看起来好像很明显,其实不明显,你也不知道哪些孩子是一个点集。所以这个就建立无向图就行了。

Cat VS Dog代码:
int T,n,m,k;
PII va[N];
int vis[N],match[N],cnt;

unordered_map<string,int > id;
vector<int > e[N];

bool dfs(int now)
{
	for(auto spot:e[now])
	{
		if(vis[spot]) continue;
		vis[spot] = 1;
		if(match[spot]==0||dfs(match[spot]))
		{
			match[spot] = now;
			return true;
		}
	}
	return false;
}

signed main()
{
	IOS;
	cin>>n>>m>>k;
	for(int i=1;i<=k;i++)
	{
		string a,b;
		cin>>a>>b;
		if(!id[a]) id[a] = ++cnt;
		if(!id[b]) id[b] = ++cnt;
		va[i] = {id[a],id[b]}; //第i个孩子喜欢的和讨厌的
	}
	for(int i=1;i<=k;i++)
	{
		for(int j=i+1;j<=k;j++)
		{
			if(va[i].fi==va[j].se||va[i].se==va[j].fi) //如果这俩孩子有矛盾,也就是a喜欢的是b讨厌的,a讨厌的是b喜欢的
			{
				e[i].pb(j); //这里为什么建立双向边,因为如果你建立单向边,但是你不知道左边的点集到底是哪些,所以就建立双边最后最大匹配除以2即可。
				e[j].pb(i);
			}
		}
	}
	int ans = 0;
	for(int i=1;i<=k;i++)
	{
		for(int j=1;j<=k;j++) vis[j] = 0;
		if(dfs(i)) ans++;
	}
	cout<<k-ans/2; //总数减去最大匹配就是最大独立集
	return 0;
}

机器任务:
题意:就是有k个任务,可以从A机器的a状态来做,可以从B机器的b状态来做。你可以随便安排任务的循序,使得两个机器调整模式的次数最少。
思考:很明显的就是用最少的点去覆盖所有边,也就是任务,把边看成任务,两个点就是不同的机器。

机器任务代码:
int T,n,m,k;
int va[N];
int vis[N],match[N];

vector<int > e[N];

bool dfs(int now)
{
	for(auto spot:e[now])
	{
		if(vis[spot]) continue;
		vis[spot] = 1;
		if(match[spot]==0||dfs(match[spot]))
		{
			match[spot] = now;
			return true;
		}
	}
	return false;
}

signed main()
{
	IOS;
	while(cin>>n&&n)
	{
		cin>>m>>k;
		for(int i=1;i<=max(n,m);i++) //两个点集初始化的时候,注意范围
		{
			vis[i] = match[i] = 0;
			e[i].clear();
		}
		for(int i=1;i<=k;i++)
		{
			int a,b,id;
			cin>>id>>a>>b;
			if(!a||!b) continue;
			e[a].pb(b); //这里就是建立的单向边,因为点集很明显了
		}
		int ans = 0;
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=m;j++) vis[j] = 0; //这里也是,对所有的清空
			if(dfs(i)) ans++;
		}
		cout<<ans<<"\n"; //这道题要看出来就是求最小点集去覆盖所有边
	}
	return 0;
}

Antenna Placement:
题意:就是一个图中有一些点,你向用最少的信号塔去覆盖所有的点,问你最少用的信号塔个数。
思考:目的是去覆盖点,很明显就是用边去覆盖所有点,那么就是最小边覆盖。

Antenna Placement代码:
int T,n,m,k;
char va[M][M];
int id[M][M],cnt;
int vis[N],match[N];
int dx[4] = {0,0,1,-1};
int dy[4] = {1,-1,0,0};

vector<int > e[N];

bool dfs(int now)
{
	for(int i=0;i<e[now].size();i++) //POJ不支持auto
	{
		int spot = e[now][i];
		if(vis[spot]) continue;
		vis[spot] = 1;
		if(match[spot]==0||dfs(match[spot]))
		{
			match[spot] = now;
			return true;
		}
	}
	return false;
}

void init()
{
	cnt = 0;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		id[i][j] = 0;
	}
	for(int i=1;i<=n*m;i++)
	{
		vis[i] = match[i] = 0;
		e[i].clear();
	}
		
}

signed main()
{
	IOS;
	cin>>T;
	while(T--)
	{
		cin>>n>>m;
		init();
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=m;j++)
			{
				cin>>va[i][j];
				if(va[i][j]=='*'&&!id[i][j]) id[i][j] = ++cnt;
			}
		}
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=m;j++)
			{
				for(int k=0;k<4;k++)
				{
					int x = i+dx[k],y = j+dy[k];
					if(x<1||x>n||y<1||y>m) continue;
					e[id[i][j]].pb(id[x][y]);
					e[id[x][y]].pb(id[i][j]);
				}
			}
		}
		int ans = 0;
		for(int i=1;i<=cnt;i++)
		{
			for(int j=1;j<=cnt;j++) vis[j] = 0;
			if(dfs(i)) ans++;
		}
		cout<<cnt-ans/2<<"\n"; //总点数减去最大匹配就是答案最小边覆盖
	}
	return 0;
}

Air Raid:
题意:就是给你一个城镇,一些边,然后让你求出最少的士兵,使得这些士兵可以把整个城镇巡逻一遍,但是士兵的路径不能相交。
思考:既然说巡逻一遍图,还不能相交,那就是最小不重复路径覆盖。由于最小路径覆盖问题,都是在有向图上面说的,所以建图的时候一定要是有向图,不能是无向图。

Air Raid代码:
int T,n,m,k;
int va[N];
int vis[N],match[N];

vector<int > e[N];

bool dfs(int now)
{
	for(int i=0;i<e[now].size();i++)
	{
		int spot = e[now][i];
		if(vis[spot]) continue;
		vis[spot] = 1;
		if(match[spot]==0||dfs(match[spot]))
		{
			match[spot] = now;
			return true;
		}
	}
	return false;
}

signed main()
{
	IOS;
	cin>>T;
	while(T--)
	{
		cin>>n>>m;
		for(int i=1;i<=n;i++)
		{
			e[i].clear();
			vis[i] = match[i] = 0;
		}
		for(int i=1;i<=m;i++)
		{
			int a,b;
			cin>>a>>b;
			e[a].pb(b); //注意新图要是有向图,不能是无向图ans/2
		}
		int ans = 0;
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=n;j++) vis[j] = 0;
			if(dfs(i)) ans++;
		}
		cout<<n-ans<<"\n"; //n-新图的最大匹配就是最小路径覆盖数
	}
	return 0;
}

Treasure Exploration:
题意:就是给几个机器人,然后让你求出用最少的机器人,可以巡逻完整张图。中间可以重复走,走的路径可以重复。
思考:很明显的最小重复路径点覆盖,对于可以重复,一般都是邻接矩阵存图然后floyd传递闭包,然后跑一边就可以了。当然,对于路径覆盖问题也是在DAG上谈的。

Treasure Exploration代码:
int T,n,m,k;
int va[N];
int dist[M][M];
int vis[N],match[N];

bool dfs(int now)
{
	for(int i=1;i<=n;i++)
	{
		int spot = i;
		if(vis[spot]||spot==now||dist[now][spot]==0) continue; //如果自己走自己也要continue
		vis[spot] = 1;
		if(match[spot]==0||dfs(match[spot]))
		{
			match[spot] = now;
			return true;
		}
	}
	return false;
}

void init()
{
	for(int i=1;i<=n;i++) vis[i] = match[i] = 0;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			if(i!=j) dist[i][j] = 0; //细节
			else dist[i][j] = 1;
		}
	}
}

signed main()
{
	IOS;
	while(cin>>n>>m)
	{
		if(n==0&&m==0) break;
		init();
		for(int i=1;i<=m;i++)
		{
			int a,b;
			cin>>a>>b;
			dist[a][b] = 1;
		}
		for(int k=1;k<=n;k++)
		{
			for(int i=1;i<=n;i++)
			{
				for(int j=1;j<=n;j++)
				dist[i][j] |= (dist[i][k]&dist[k][j]);
			}
		}
		int ans = 0;
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=n;j++) vis[j] = 0;
			if(dfs(i)) ans++;
		}
		cout<<n-ans<<"\n"; //n-传递闭包后的最大匹配就是最小重复路径覆盖
	}
	return 0;
}

Kindergarten:
题意:就是有n个男生和m个女生,然后所有的男生都相互认识,所有的女生也都相互认识。现在又给你k个关系,某些男生认识某些女生,然后问你最多能选择多少人,使得任意两个人之间都互相认识。
思考:这个也很明显吧,男生女生之间是不同的点集,但是你发现这违规了二分图啊,男生男生之间认识这不就是内部有边了?对啊,但是这题是让你求最大的,任意两个人都相互认识,所以求的是最大团。直接对图建立反图,然后求最大匹配然后直接减去就可以了。

Kindergarten代码:

int T,n,m,k;
int va[N];
int can[M][M];
int vis[N],match[N];

vector<int > e[N];

bool dfs(int now)
{
	for(int i=0;i<e[now].size();i++)
	{
		int spot = e[now][i];
		if(vis[spot]) continue;
		vis[spot] = 1;
		if(match[spot]==0||dfs(match[spot]))
		{
			match[spot] = now;
			return true;
		}
	}
	return false;
}

void init()
{
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		can[i][j] = 0;
	}
	for(int i=1;i<=n+m;i++)
	{
		vis[i] = match[i] = 0;
		e[i].clear();
	}	
}

signed main()
{
	IOS;
	int cs = 1;
	while(cin>>n>>m>>k&&n&&m&&k)
	{
		init();
		while(k--)
		{
			int a,b;
			cin>>a>>b;
			can[a][b] = 1;
		}
		for(int i=1;i<=n;i++) //这里题目已经很明确的说了,两个点集,那么i和j尽管都是1的时候,也是代表的不同的节点,所以不要混乱。就是1号女孩和1号男孩的事。并不是自己和自己,所以下面建立的图就是二分图,不同的点集,所以ans不用除以2。
		{
			for(int j=1;j<=m;j++)
			{
				if(!can[i][j]) //如果有边就不看,要的就是补图
				e[i].pb(j); 
			}
		}
		int ans = 0;
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=m;j++) vis[j] = 0;
			if(dfs(i)) ans++;
		}
		cout<<"Case "<<cs++<<": "<<n+m-ans<<"\n"; //最大团就是补图的最大独立集
	}
	return 0;
}

Spy:
题意:给你一个n,再给出含有n个数的数组a,p,b,c。现在,你可以随意的将数组b和c中的数两两配对求和(总共有n!种配法),假设配对后的数组为d。然后再把a和d中的数两两配对比较。如果d[j]>a[i],那么可以获得p[i]单位的荣誉值,要求两两配对比较获得的荣誉值的和尽可能的大。求所能获得的荣誉值的期望乘以n,题目保证乘以n后答案为整数。
思考:我们考虑一个配对比较(d[j],a[i]),如果d[j]>a[i],那么他对期望的贡献为p[i]/(n!)。假设b[x]+c[y]==d[j],那么他俩配对的配法有(n-1)!种,所以这种情况对期望的贡献为p[i]/(n!)(n-1)!,又因为答案要求的乘n,所以对答案的贡献就变为p[i]/(n!)(n-1)!*n=p[i],正好为p[i]。所以求的就是最大权匹配。直接套用KM的板子就行了。

Spy代码:
int n;
int a[N],b[N],c[N],p[N];
int w[N][N];
bool visy[N];
int lx[N],ly[N],linker[N],slack[N],pre[N];

void bfs(int k)
{
	int x = 0,y = 0,yy = 0,delta = 0;
	for(int i=0;i<N;i++)
	{
		pre[i] = 0;
		slack[i] = inf;
	}
    linker[y] = k;
    while(1)
	{
        x = linker[y]; delta = inf; visy[y] = true;
        for(int i=1;i<=n;i++)
		{
            if(!visy[i])
			{
                if(slack[i]>lx[x]+ly[i]-w[x][i])
				{
                    slack[i] = lx[x]+ly[i]-w[x][i];
                    pre[i] = y;
                }
                if(slack[i] < delta) delta = slack[i],yy = i;
            }
        }
        for(int i=0;i<=n;i++)
		{
            if(visy[i]) lx[linker[i]] -= delta,ly[i] += delta;
            else slack[i] -= delta;
        }
        y = yy;
        if(linker[y]==-1) break;
    }
    while(y) linker[y] = linker[pre[y]],y = pre[y];
}
 
int KM()
{
	for(int i=0;i<N;i++) 
	{
		lx[i] = ly[i] = 0;
		linker[i] = -1;
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<N;j++) visy[j] = 0;
		bfs(i);
	}
	int res = 0;
	for(int i=1;i<=n;i++)
	{
		if(linker[i]!=-1)
		res += w[linker[i]][i];
	}
    return res;
}
 
signed main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++) cin>>p[i];
    for(int i=1;i<=n;i++) cin>>b[i];
    for(int i=1;i<=n;i++) cin>>c[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            int sum = 0;
            for(int k=1;k<=n;k++) if(b[i]+c[j]>a[k]) sum += p[k];
            w[i][j] = sum;
        }
    }
    cout<<KM();
    return 0;
}

总结:
反正多多思考吧,多多理解里面的本质什么的。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值