-
图论总结
-
补图
P3452[POI2007]BIU-Offices
题意:给出一个图(N个点,M条边),让你把此图分成尽可能多的集合,满足任意不在同一集合的点之间都有边相连。
考虑对于原图:若x,y有连边,则x,y不一定不在同一个集合。
若x,y无连边,则通过题意,x,y一定在同一个集合。
故我们建出原图的补图(全集为完全图),这样就变成了找补图里的联通块个数。
怎样建图?首先O(n^2)暴力建边肯定TLE,因此我们考虑维护集合S,每次扩展一个未加入补图联通块的点x进行BFS,从集合S中选出当前点x扩展不到的点进行入队,扩展到的点还原回S,这样便不用建图也可以找出联通块。
code:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<vector>
using namespace std;
const int N=100010;
const int M=2000010;
vector<int>S;
vector<int>g[N];
vector<int>ans;
bool vis[N],h[N];
int n,m;
int bfs(int s)
{
int tot=0;
queue<int>q;
q.push(s);
h[s]=1;//s被访问
while(q.size())
{
int u=q.front();q.pop();
vis[u]=1;tot++;//联通块大小+1
for(int i=0;i<g[u].size();i++)h[g[u][i]]=1;//标记访问到的点
vector<int>tmp=S;S.clear();
for(int i=0;i<tmp.size();i++)
{
if(h[tmp[i]])S.push_back(tmp[i]);//被访问的点不被拿出拓展其他点
else q.push(tmp[i]);//不被访问说明没有连边(即补图有边)入队
}
for(int i=0;i<g[u].size();i++)
h[g[u][i]]=0;//标记撤销
}return tot;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
g[x].push_back(y);
g[y].push_back(x);
}
for(int i=1;i<=n;i++)S.push_back(i);//未扩展的节点
for(int i=1;i<=n;i++)
if(!vis[i])ans.push_back(bfs(i));//bfs返回补图联通快大小
sort(ans.begin(),ans.end());
cout<<ans.size()<<endl;
for(int i=0;i<ans.size();i++)
cout<<ans[i]<<" ";
}
-
图的连通性
并查集缩点:
对于被删的边$i$,有两种情况。
1、不在$mst$上,不做处理直接输出权值和
2、在$mst$上,我们考虑把一条非树边加入$mst$中使其联通。
删去的边$i$把$mst$分成两个集合$A,B$,我们要找的就是集合$A$和$B$之间所有连边中最小的。
怎样找?我们把边从小到大排序,容易发现对于一条边$u->v$,若其满足条件,则路径$u->lca->v$会覆盖被删除的边。于是我们用书上带权并查集维护$minx$表示其上方的边被覆盖的最小权值,被更新过的点由于并查集的性质,再一次访问到的时候会直接跳过,这也就是并查集缩点。
code:
#include<bits/stdc++.h>
using namespace std;
const int N=50010;
const int M=100010;
struct edge
{
int u,v,w,id;
}e[M],fir[M];
int cnt;
int n,m,q;
bool on_mst[M];
int ans;
int minw[N],dep[N],f[N];
vector<int>g[N];
struct DSU
{
int father[N];
int find(int x){return x==father[x]?x:father[x]=find(father[x]);}
void merge(int u,int v){int r1=find(u),r2=find(v);father[r1]=r2;}
bool check(int u,int v){return find(u)==find(v);}
void init(int x){for(int i=1;i<=x;i++)father[i]=i;}
}dsu;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
bool cmp(edge a,edge b)
{
return a.w<b.w;
}
void kruskal()
{
cnt=0;ans=0;
dsu.init(n);
sort(e+1,e+1+m,cmp);
for(int i=1;i<=m;i++)
{
int u=e[i].u,v=e[i].v,w=e[i].w;
int r1=dsu.find(u),r2=dsu.find(v);
if(r1!=r2)
{
on_mst[e[i].id]=1;
cnt++;ans+=w;
dsu.father[r1]=dsu.father[r2];
g[u].push_back(v);
g[v].push_back(u);
}
}
}
void dfs(int u,int fa)
{
f[u]=fa;
dep[u]=dep[fa]+1;
for(int i=0;i<g[u].size();i++)
{
int v=g[u][i];
if(v==fa)continue;
dfs(v,u);
}
}
void cover(int x,int y,int w)
{
x=dsu.find(x);y=dsu.find(y);
while(x!=y)
{
if(dep[x]<dep[y])swap(x,y);
dsu.merge(x,f[x]);
minw[x]=w;
x=dsu.find(f[x]);
}
}
int main()
{
n=read();m=read();
for(int i=1;i<=m;i++)
{
int x=read(),y=read(),z=read();
fir[i].u=e[i].u=x;
fir[i].v=e[i].v=y;
fir[i].w=e[i].w=z;
fir[i].id=e[i].id=i;
}
kruskal();
if(cnt!=n-1)
{
q=read();
while(q--)
{
cout<<"Not connected"<<endl;
}return 0;
}
dfs(1,0);
memset(minw,-1,sizeof(minw));
dsu.init(n);
for(int i=1;i<=m;i++)
{
if(on_mst[e[i].id])continue;
cover(e[i].u,e[i].v,e[i].w);
}
q=read();
while(q--)
{
int id=read();
if(!on_mst[id])printf("%d
",ans);
else
{
int x=fir[id].u,y=fir[id].v,w=fir[id].w;
if(dep[x]<dep[y])swap(x,y);
if(minw[x]==-1)cout<<"Not connected"<<endl;
else printf("%d
",ans-w+minw[x]);
}
}
}
$Tarjan$算法:
1、点双联通分量
2、边双联通分量
P2860 [USACO06JAN]冗余路径Redundant Paths
我们把无向图的环通过$Trajan$算法缩成点,这样原图就变成了一棵树。把叶子节点两两连接即可。
3、强联通分量
-
最短路
1、$spfa$
2、$dijkstra$
-
$MST$(最小生成树)
1、$Boruvka$:
$Boruvka$是一种和$Kruskal$不同的求解$MST$的方法,其思想是:
对于每个点找出其连出的最小的边(这一步复杂度可以不和$m$相关)
对这些边做一次$Kruskal$
将同一个联通块的点缩起来,重复第一步,直到只剩一个点为止
由环切性质,容易发现这个算法是正确的
分析一下复杂度,选出来的边构成了一个基环树森林,每次$Kruskal$做完后剩下的点数就是基环树的个数,由于每个点都引出了一条边,于是个数最多的情况是若干个孤立的二元环,也就是说每次迭代点数至少减少一半,于是至多$logn$次就能结束算法。而每次$Kruskal$的复杂度是$O(n+m)$的(只用最开始排一次序),故总复杂度为O((n+m)logn)。
2、$Kruskal$: