图论
连通性
什么是连通?
任意两点之间有路径使其相互连接。
强连通分量
- 若一张有向图的节点两两互相可达,则称这张图是强连通的
- 强连通分量是一个图的一部分是强联通的,则称这是强连通分量
怎么求?
这里给出一种方法:tarjan
- Tarjan 算法基于对图进行 深度优先搜索。我们视每个连通分量为搜索树中的一棵子树,在搜索过程中,维护一个栈,每次把搜索树中尚未处理的节点加入栈中。
对于某个节点u,我们需要维护以下变量 - low[u],当前节点不经过父亲能返回到的最早的节点是什么
- dfn[u],时间戳,表示是第几个被访问到的
- black[u],在第几个强连通分量中。
思路:
- 要是没有时间戳,即没有被访问过,那就遍历这一个点,然后因为当前节点可以遍历到下一个节点,放到栈中,所以low[u]可以是下一个节点或自身的最小值。
- 否则就是当前节点或者下一个节点可以返回的最小值
- 最后如果当前节点的时间戳与low相同,则说明走了一圈又绕回来了,那么当前在栈中的就属于当前这个强连通分量。
void tarjan(int x){
st.push(x);
dfn[x]=low[x]=++cnt;
for(int i=head[x];i!=-1;i=a[i].nxt){
int to=a[i].to;
if(!dfn[to]){
tarjan(to);
low[x]=min(low[x],low[to]);
}
else {
if(!black[to]){
low[x]=min(low[x],dfn[to]);
}
}
}
if(dfn[x]==low[x]){
black[x]=++sum,q[sum].push_back(x);
while(st.top()!=x){
black[st.top()]=sum;
q[sum].push_back(st.top());
st.pop();
}
st.pop();
}
}
缩点
- 把所有强连通分量都缩成一个点就可以了
- 与强连通分量代码类似
割点
- 在无向图中将这一点删去后,无法使图强连通。
定义
- low[u],当前节点不经过父亲能返回到的最早的节点是什么
- dfn[u],时间戳,表示是第几个被访问到的
思路:
- 因为删去之后无法连通,所以这个点的子节点最早可以到达的点一定会比割点的时间戳要大或相等,不然就可以走其他点过去。
- 当没有遍历过时,将这一个点遍历,判断它是否满足要求,并且将它的能不通过父节点能到达的最早的点通过子节点更新。
- 如果遍历过,并且这个点的子节点不是他的父节点,那么更新
- 最后有一种特殊情况,即当一个点只能通过它到达的节点的数量>=2且它没有父节点,所以无法被上面更新到,如果满足以以上要求,就更新
#include<bits/stdc++.h>
#include<stack>
#include<vector>
using namespace std;
const int N=5e5+10;
struct node{
int to,nxt;
}a[N<<1];
int n,m,u,v;
int tot,head[N<<1];
void add(int x,int y){
a[++tot].to=y;
a[tot].nxt=head[x];
head[x]=tot;
}
int low[N<<1],dfn[N<<1];
priority_queue<int>q;
int cnt;
int vis[N],flag[N];
void tarjan(int now,int fa){
int son=0;
low[now]=dfn[now]=++cnt;
for(int i=head[now];i;i=a[i].nxt){
int to=a[i].to;
if(!dfn[to]){
++son;
tarjan(to,now);
low[now]=min(low[now],low[to]);
if(low[to]>=dfn[now]&&(!flag[now])&&(fa!=now)){
flag[now]=1;q.push(-now);
}
}
else if(to!=fa){
low[now]=min(low[now],dfn[to]);
}
}
if(son>=2&&(!flag[now])&&(fa==now)){
flag[now]=1;q.push(-now);
// cout<<now<<" ";
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(NULL),cout.tie(NULL);
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>u>>v;
add(u,v),add(v,u);
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
tarjan(i,i);
}
}
cout<<q.size()<<'\n';
while(q.size()){
cout<<-1*q.top()<<" ";q.pop();
}
return 0;
}
割边
- 在无向图中,删去一点使得整张图不强连通,则这个边为割边
- 因为这个边为割边,所以一定有这个边连接的父节点的时间戳要比子节点最早能到的时间要小,不能取等是因为,如果通过其他点也能到达,那么则其他边可以到达,则不是割边。
- 所以满足时间戳要比子节点最早能到的时间要小,连接的两个点的边是割边,将无向图的两个边通过异或的方式标记。
- 类似割点一样的的改变边,但要注意,标记过时的修改条件是不是反向边。
- 随后统计答案即可
void tarjan(int x,int edge){
dfn[x]=low[x]=++cnt;
for(int i=head[x];i;i=a[i].nxt){
int to=a[i].to;
if(!dfn[to]){
tarjan(to,i);
if(low[to]>dfn[x]){
flag[i]=flag[i^1]=1;
}
low[x]=min(low[x],low[to]);
}
else if(i!=(edge^1)){
low[x]=min(low[x],low[to]);
}
}
}
双连通分量
边双连通分量
- 一个只删掉一条边后仍然可以连通的图
- 由定义得,边双的边界一定是割边,不然还可以继续扩展
- 由上面的性质还可以得知每一个点一定只在一个边双中,因为若在两个中就不满足边界是割边。
- 所以最后把割边去除后,再给每一个没标记过的点,跑一遍不经过标记过点的dfs就可以
#include<bits/stdc++.h>
using namespace std;
const int N=5e6+10;
int n,m,u,v;
int cnt,sum;
int dfn[N],low[N];
struct node{
int to,nxt;
}a[N<<1];
bool flag[N];
int tot=1,head[N<<1];
void add(int x,int y){
a[++tot].to=y;
a[tot].nxt=head[x];
head[x]=tot;
}
void tarjan(int x,int edge){
dfn[x]=low[x]=++cnt;
for(int i=head[x];i;i=a[i].nxt){
int to=a[i].to;
if(!dfn[to]){
tarjan(to,i);
if(low[to]>dfn[x]){
flag[i]=flag[i^1]=1;
}
low[x]=min(low[x],low[to]);
}
else if(i!=(edge^1)){
low[x]=min(low[x],low[to]);
}
}
}
int black[N<<1];
vector<int>st[N<<1];
void dfs(int now,int id){
black[now]=id;
st[id].push_back(now);
for(int i=head[now];i;i=a[i].nxt){
int to=a[i].to;
if((black[to]||(flag[i]))) continue;
dfs(to,id);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>u>>v;
if(u==v) continue;
add(u,v),add(v,u);
}
for(int i=1;i<=n;i++){
if(!dfn[i])
tarjan(i,0);
}
int ans=0;
for(int i=1;i<=n;i++){
if(!black[i]){
dfs(i,++ans);
}
}
cout<<ans<<'\n';
for(int i=1;i<=ans;i++){
cout<<st[i].size()<<" ";
for(int j=0;j<st[i].size();j++){
cout<<st[i][j]<<" ";
}
cout<<'\n';
}
return 0;
}
点双联通分量
- 删掉一个点不能使得这一个图不连通。
- 因为是点,所以有可能一个点出现在两个点双中,观察发现一个点双其实是上一个割点或者根节点到这一个割点的所有点的集合。
- 所以可以先把经过的点放进栈中,然后当这个节点是根或者割点时弹出。
void tarjan(int now,int fa){
low[now]=dfn[now]=++cnt;
if(!head[now]){
scc[++sum].push_back(now);
}
st.push(now);
for(int i=head[now];i;i=a[i].nxt){
int to=a[i].to;
if(!dfn[to]){
tarjan(to,now);
low[now]=min(low[now],low[to]);
if(low[to]>=dfn[now]){
++sum;
int t;
while(t!=to){
t=st.top();
st.pop();
scc[sum].push_back(t);
}
scc[sum].push_back(now);
}
}
else if(to!=fa){
low[now]=min(low[now],dfn[to]);
}
}
}
例题:
BLO-Blockade
观察题目可以得到一些结论
- 当一个点不是割点时,则不连通的就是它和其他所有点
- 当一个点是割点时,则可以形象的把他看作一个树的根,那么显然的应该是它的所有"子树"连通块的与其他联通块的乘积,但注意不能重复,所以只需要一个与已经经过过的子树相乘即可
- 最后注意这样统计的是无序点对,有序点对要乘以2
#include<bits/stdc++.h>
#include<stack>
#include<vector>
#define int long long
using namespace std;
const int N=5e6+10;
struct node{
int to,nxt;
}a[N<<1];
int n,m,u,v;
int tot,head[N<<1];
void add(int x,int y){
a[++tot].to=y;
a[tot].nxt=head[x];
head[x]=tot;
}
int low[N<<1],dfn[N<<1];
int cnt;
int vis[N],flag[N];
int ans[N];
int siz[N];
void tarjan(int now,int fa){
int son=0;
low[now]=dfn[now]=++cnt;
int sum=0;
siz[now]=1;
for(int i=head[now];i;i=a[i].nxt){
int to=a[i].to;
if(!dfn[to]){
++son;
tarjan(to,now);
siz[now]+=siz[to];
low[now]=min(low[now],low[to]);
if(low[to]>=dfn[now]){
ans[now]+=siz[to]*sum;
sum+=siz[to];
}
}
else if(fa!=to){
low[now]=min(low[now],dfn[to]);
}
}
ans[now]+=(n-sum-1)*sum+(n-1);
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(NULL),cout.tie(NULL);
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>u>>v;
add(u,v),add(v,u);
}
tarjan(1,1);
for(int i=1;i<=n;i++){
cout<<ans[i]*2<<'\n';
}
return 0;
}
受欢迎的牛 G
- 读题得这道题有传递性,在有向图中,且针对每一个点,可以缩点
- 当只有一个连通块时,它一定是所有的牛。
- 当缩完点后,有两个及以上的出度为零,则说明不可能所有牛都喜欢,因为有一个连通块被隔开了。
- 最后,如果有一个出度为零,那么只有这一个块中的牛,会被所有牛喜欢
#include<bits/stdc++.h>
#include<stack>
#include<vector>
using namespace std;
int n,m,u,v,tot,cnt,tmp;
int head[10005],dfn[10005],low[10005],c[10005],du[2001000],kkk[2000010];
stack<int>st;
vector<int>cok[10005];
bool vis[10005],s[10005];
struct node{
int to,nxt;
}a[100005];
void add(int x,int y){
a[++tot].to=y;
a[tot].nxt=head[x];
head[x]=tot;
}
void tarjan(int x){
dfn[x]=low[x]=++tmp;
st.push(x);
s[x]=1;
for(int i=head[x];i;i=a[i].nxt){
int f=a[i].to;
if(!dfn[f]){
tarjan(f);
low[x]=min(low[x],low[f]);
}
else if(s[f]){
low[x]=min(low[x],dfn[f]);
}
}
if(dfn[x]==low[x]){
cnt++;
int d=0;
while(x!=d){
d=st.top();
st.pop();
s[d]=0;
kkk[cnt]++;
c[d]=cnt;
}
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>u>>v;
add(u , v);
}
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i);
for(int i=1;i<=n;i++){
for(int j=head[i];j;j=a[j].nxt){
if(c[i]!=c[a[j].to]){
du[c[i]]++;
}
}
}
int num = 0,suu=0;
for(int i=1;i<=cnt;i++){
if(!du[i]){
num=i;
suu++;
}
}
if(suu>=2)
cout<<0;
else cout<<kkk[num];
return 0;
}
拓扑排序
- 在一个有向无环图中,我们将图中的顶点以线性方式进行排序,使得对于任何的顶点 u 到 v 的有向边 (u,v), 都可以有 u 在 v 的前面。
- 可以使图变得有序
如何实现?
- 可以把每一个点的入度统计,然后把入度为零,即前面没有点的点加入队列,然后在队列中,把入度为零的点弹出,然后再将它所连接的点的入度减一
- 重复以上操作即可
for(int i=1;i<=n;i++){
if(!ru[i]){
cout<<i<<" ";
q.push(i);
}
}
while(!q.empty()){
int fr=q.front();
q.pop();
for(int i=head[fr];i;i=a[i].nxt){
ru[a[i].to]--;
if(!ru[a[i].to]){
cout<<a[i].to<<" ";
q.push(a[i].to);
}
}
}
例题:
最大食物链计数
- 把一个点到另一个点的边看作是吃与被吃。
- 求最大食物链有多少。
- 考虑在拓扑序下统计答案,对于每一个点都可以由上一个点转移过来,答案累加上一个点。
- 最后每个出度为0的点累加答案
#include<bits/stdc++.h>
#define mod 80112002
using namespace std;
long long tot,m,head[501000],n,p[510010],cu[510010],u,v,dp[510000];
struct node{
long long to,nxt;
}a[510000];
void add(int x,int y){
a[++tot].to=y;
a[tot].nxt=head[x];
head[x]=tot;
}
queue<int>q;
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>u>>v;
add(u,v);
p[v]++;
cu[u]++;
}
for(int i=1;i<=n;i++){
if(!p[i]){
dp[i]++;
q.push(i);
}
}
while(!q.empty()){
int fr=q.front();
q.pop();
for(int i=head[fr];i;i=a[i].nxt){
dp[a[i].to]=(dp[a[i].to]+dp[fr])%mod;
p[a[i].to]--;
if(!p[a[i].to]){
q.push(a[i].to);
}
}
}
long long ans=0;
for(int i=1;i<=n;i++){
if(!cu[i]){
ans=(ans+dp[i])%mod;
}
}
cout<<ans;
return 0;
}
- 模拟拓扑排序过程即可
#include<bits/stdc++.h>
using namespace std;
int n;
int u,v,m;
int ru[510];
queue<int>q;
struct node{
int nxt,to;
}a[20010];
int head[10020],tot;
void add(int x,int y){
a[++tot].to=y;
a[tot].nxt=head[x];
head[x]=tot;
}
bool flag[510];
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>u>>m;
flag[u]=1;
while(m--){
cin>>v;
add(u,v);
ru[v]++;
}
}
int ans=0;
for(int i=1;i<=500;i++){
if(!ru[i]&&(flag[i])){
q.push(i);++ans;
}
}
while(q.size()){
int fr=q.front();
q.pop();
for(int i=head[fr];i;i=a[i].nxt){
int to=a[i].to;
ru[to]--;
if(!ru[to]&&(flag[to])){
++ans;q.push(to);
}
}
}
if(ans==n){
cout<<"YES";
}
else cout<<n-ans;
return 0;
}
最短路
单源最短路
- 一般使用dijkstra算法,是一种贪心的思路,但是不能运用在有负环的情况下
- spfa算法,也是一种最短路算法,优点是可以处理负边权,缺点是时间最坏O(nm).
spfa怎么写?
- spfa 是通过立一个队列,初始时队列里只有起始点,在建立一个表格记录起始点到所有点的最短路径。
- 然后执行松弛操作,用队列里有的点去刷新起始点到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空。
void spfa(){
queue<int>q;
for(int i=1;i<=n;i++){
dist[i]=INT_MAX;
}
dist[s]=0;
flag[s]=1;
q.push(s);
while(!q.empty()){
int fr=q.front();
q.pop();
flag[fr]=0;
for(int i=head[fr];i;i=a[i].nxt){
int to=a[i].to,w=a[i].w;
if(dist[to]>w+dist[fr]){
dist[to]=w+dist[fr];
if(!flag[to]){
q.push(to);
}
flag[to]=1;
}
}
}
}