文章目录
搜索与图论(三)
检查是否有最小生成树
检查是否有二分图
最小生成树
prim算法
prim算法(朴素版——稠密图m>n^2 )
(注:到集合的距离——指的是一个点到一个集合当中任意一个点的距离,取其中最短的距离作为点到集合的距离(如果没有那么距离为无穷))如图:
算法流程:
1将dist数组初始化为无穷(与dijkstra算法不同的是这个dist数组是点到集合的距离,而不是点到原点的距离)
2遍历n个点找到一个到集合st中距离最近的点(找到的第一个点比较特殊,它是无穷但同样加入dist,对这个点要特殊处理)
3根据这个点更新其他点到集合的距离
4把这个点加入st
858prim算法求最小生成树
最小生成树的概念:最小生成树是原图的最小连通子图
即从原图中抽出来最少的边,让所有点连通
例:抽出的最小生成树(不唯一)
#include <iostream>
#include <cstring>
#include<algorithm>
using namespace std;
const int N=510;
int dist[N]; //dist存储的是点到集合的距离
int g[N][N]; //邻接矩阵
bool st[N];
int n,m;
int prim(){
int res=0; //res是最小生成树边权之和
memset(dist, 0x3f,sizeof dist); //dist全部初始化为无穷
for(int i=0;i<n;i++){ //找一个距离集合最小的点,第一次都是无穷,随机加入一个点
int s=-1;
for(int j=1;j<=n;j++)
if(!st[j]&&(s==-1||dist[s]>dist[j]))s=j; //找点操作
if(i&&dist[s]==0x3f3f3f3f)return 0x3f3f3f3f; //如果找出来的不是第一个点且距离集合为无穷,说明不存在通路,没有最小生成树
for(int j=1;j<=n;j++)dist[j]=min(dist[j],g[s][j]); //根据这个点更新其他点到集合的距离,注意dijkstra是更新到原点的距离
st[s]=true; //标记找过的最短距离点
if(i)res+=dist[s]; //边权之和 每次要加上找到的点到集合的距离
}
return res;
}
int main(){
cin>>n>>m;
memset(g,0x3f,sizeof g);
while(m--){
int a,b,c;
cin>>a>>b>>c;
if(a!=b)g[a][b]=g[b][a]=min(g[a][b],c); //赋初值时不仅要去重边,还要去自环
}
int t=prim();
if(t==0x3f3f3f3f)cout <<"impossible";
else cout<<t;
return 0;
}
kruskal算法
只需要枚举每条边(和floyd算法类似)所以使用简单的结构体
注:将a,b边加入集合中就是在a,b中间加一条边
859 kruskal算法求最小生成树
#include <iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e5+10,M=200050;
int n,m;
int p[N],cnt[N]; //并查集p,已经连通边的计数cnt
int find(int x){ //并查集寻找个祖宗节点模板
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
struct Edge{
int a,b,w;
bool operator <(const Edge &W)const{ //操作符<重载
return w<W.w;
}
}edges[M];
int main(){
cin>>n>>m;
for(int i=0;i<m;i++){ //边初始化
int a,b,w;
cin>>a>>b>>w;
edges[i]={a,b,w};
}
for(int i=1;i<=n;i++)p[i]=i; //并查集初始化
sort(edges,edges+m); //对m条边根据边权进行排序
int cnt=0,res=0; //cnt记录当前连接了多少条边,res记录最小生成树的边权之和
for(int i=0;i<m;i++){
int a=edges[i].a,b=edges[i].b,w=edges[i].w;
int pa=find(a) ,pb=find(b);
if(pa!=pb){ //如果遍历到的边对应的两个点不在一个集合中(即两点间没有连一条边)把他们连起来(归到一个集合)并且使计数++,边权加w
p[pa]=pb;
cnt++;
res+=w;
}
}
if(cnt<n-1) cout<<"impossible"; //如果最后连接的边小于n-1(因为有n个点,所以要连接n-1条边)就不存在最小生成树
else cout<<res;
return 0;
}
二分图
二分图:如果一个图能分成两个集合,并且两个集合中的点互相连通,而各集合内部没有连通的边,就为二分图 如图:
奇数环:一个环内有奇数条边
染色法
流程:
遍历n个点
如果 i没有染色,将它染色,找下一个点j,并染下一个临点dfs(j,1),检查是否染成功
如果整张图都成功染色没有出现矛盾,说明是二分图否则不是
矛盾:染色后如果一条边的两个点属于同一颜色(集合),则出现矛盾
860 染色法判定二分图
挨个点判定所属集合,如果没有矛盾则是二分图
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e5+10,M=2*N;
int h[N],ne[M],e[M],idx; //稀疏图
int color[N];
int n,m;
bool dfs(int u,int c){ //从u节点开始,向他的子节点延伸并逐个染成颜色c,如果没有矛盾返回true
color[u]=c;
for(int i=h[u];i!=-1;i=ne[i]) //找u的所有子节点
{
int j=e[i];
if(!color[j]) //如果j没有被染过色(注意图中其他头节点的子节点也有可能包含j,j可能在其他节点染了色)将它染色
{
if(!dfs(j,3-c))return false; //3-c将j染成与u相反的颜色(u染成1,3-c染成2,u染成2,3-c则染成1)如果失败返回false
}
else if(color[j]==c)return false; //如果j已经染色且与u颜色相同,返回flase
}
return true; //没有矛盾,返回true
}
void insert (int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--){
int a,b;
cin>>a>>b;
insert (a,b);
insert(b,a);
}
bool flag =true; //flag=true表示没有矛盾
for(int i=1;i<=n;i++) //将每一个节点都当做头结点向下遍历,进行染色
{
if(!color[i]) //如果i未染色将它染色
{
if(!dfs(i,1)) //出现矛盾flag=false
{
flag=false;
break;
}
}
}
if(flag)cout<<"Yes";
else cout<<"No";
return 0;
}
和普通爆搜区别在于dfs是**多源搜索(不是只从1开始搜索)**要从1遍历到n搜索不然可能无法遍历所有路径
附上bfs代码
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int N=1e5+10,M=2*N;
int h[N],ne[M],e[M],idx; //稀疏图
int color[N];
int n,m;
bool bfs(){
queue<int> q;
for(int i=1;i<=n;i++){
if(!color[i]){
color[i]=1;
q.push(i);
while(q.size())
{
auto t=q.front();
q.pop();
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
if(!color[j]){
color[j]=3-color[t];
q.push(j);
}
else if(color[j]==color[t])return false;
}
}
}
}
return true;
}
void insert (int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--){
int a,b;
cin>>a>>b;
insert (a,b);
insert(b,a);
}
if(!bfs())cout<<"No";
else cout<<"Yes";
return 0;
}
匈牙利算法
条件:给定一个二分图,找两个集合中最大匹配的数量
匹配:即两个集合中的点都有对应唯一匹配的点,不能脚踏两条船
思路:选定其中一个集合(n1)
遍历其中所有点1~n1
将st(标记)初始化成false(防止死循环)
为每一个点寻找匹配,每找到一个匹配res++
匹配过程:
遍历n1点的所有子节点
如果这个点没有被匹配或者(已经被匹配但可以更换匹配(回找))
就达成一个匹配
匹配成功返回true
861 二分图的最大匹配
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=510,M=100010;
int h[N],e[M],ne[M],idx;
int n1,n2,m;
int match[N]; //标记n2对应的n1
bool st[N]; //标记
void insert (int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
//匹配过程
bool find(int x){
for(int i=h[x];i!=-1;i=ne[i]){
int j=e[i];
if(!st[j]){
st[j]=true; //遍历过这个节点,把他赋成true(防止find在回找的时候,仍然遍历这个点造成死循环)
if(match[j]==0||find(match[j])){ //如果j(n2中)没有对象或者是j的男朋友(n1中)能换个对象(换对象就是回找的过程,标记st防止回找的时候又把j赋给那个节点,造成死循环),就把j匹配给x
match[j]=x;
return true;
}
}
}
return false; //如果所有子节点都匹配不上,就返回失败
}
int main(){
cin>>n1>>n2>>m;
memset(h,-1,sizeof h);
while(m--){ //a一定是n1中的节点,b一定是n2中的节点
int a,b;
cin>>a>>b;
insert (a,b);
}
int res=0;
for(int i=1;i<=n1;i++){
memset(st,false,sizeof st); //重要:在每个点找匹配之前要把标记格式化(每个点都要遍历自己的子节点寻找匹配,如果没有重新初始化st,就无法重头找起)
if(find(i))res++; //能有匹配,结果加一
}
cout<<res;
return 0;
}