图论·强连通分量
强连通分量
一、Tarjan
关于拓扑序:\(tarjan\)后拓扑序是倒序,所以直接倒序循环就相当于跑拓扑序了。原理待补
复杂度 \(O(n+m)\)
板子代码:
void dfs(int x)
{
dfn[x]=low[x]=++tme;//时间戳
st.push(x);
int _size=e[x].size();
for (int i=0;i<_size;i++)
{
int v=e[x][i];
if (!dfn[v]) dfs(v),low[x]=min(low[x],low[v]);
else if (!col[v]) low[x]=min(low[x],dfn[v]);
}
if (dfn[x]==low[x])
{
vis[x]=++cnt;
sum[cnt]+=c[x];
while (st.top()!=x)
{
vis[st.top()]=cnt;
sum[cnt]+=c[st.top()];
st.pop();
}
st.pop();
}
}
void tarjan()
{
for (int i=1;i<=n;i++)
{
if (!dfn[i]) dfs(i);
}
}
void build()//缩点建图
{
for (int i=1;i<=n;i++)
{
int _size=e[i].size();
for (int j=0;j<_size;j++)
{
int v=e[i][j];
if (col[i]!=col[v]) e2[col[i]].push_back(col[v]);
}
}
}
【YbtOj】例题
A.有向图缩点
板子题,\(Tarjan\)缩点后拓扑序跑\(dp\)统计即可
贴
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
int n,m;
vector <int> e[N];
vector <int> e2[N];
int ind[N];
int c[N];
int ans;
int dfn[N],low[N];
int vis[N];
stack <int> st;
int tme;
int sum[N],cnt;
void dfs(int x)
{
dfn[x]=low[x]=++tme;
st.push(x);
int _size=e[x].size();
for (int i=0;i<_size;i++)
{
int v=e[x][i];
if (!dfn[v])
{
dfs(v);
low[x]=min(low[x],low[v]);
}
else if (!vis[v]) low[x]=min(low[x],low[v]);
}
if (dfn[x]==low[x])
{
vis[x]=++cnt;
sum[cnt]+=c[x];
while (st.top()!=x)
{
vis[st.top()]=cnt;
sum[cnt]+=c[st.top()];
st.pop();
}
st.pop();
}
}
void build()
{
for (int i=1;i<=n;i++)
{
int _size=e[i].size();
for (int j=0;j<_size;j++)
{
int v=e[i][j];
if (vis[i]!=vis[v]) e2[vis[i]].push_back(vis[v]),ind[vis[v]]++;
}
}
}
queue <int> q;
int f[N];
signed main()
{
scanf("%lld%lld",&n,&m);
for (int i=1;i<=n;i++) scanf("%lld",&c[i]);
for (int i=1,u,v;i<=m;i++)
{
scanf("%lld%lld",&u,&v);
e[u].push_back(v);
}
for (int i=1;i<=n;i++)
{
if (dfn[i]==0) dfs(i);
}
build();
for (int i=1;i<=cnt;i++)
{
if (ind[i]==0) q.push(i),f[i]=sum[i];
}
while (!q.empty())
{
int u=q.front();
q.pop();
int _size=e2[u].size();
for (int i=0;i<_size;i++)
{
int v=e2[u][i];
f[v]=max(f[v],f[u]+sum[v]);
ind[v]--;
if (!ind[v]) q.push(v);
}
}
for (int i=1;i<=cnt;i++) ans=max(ans,f[i]);
printf("%lld",ans);
return 0;
}
B.受欢迎的牛
可以想到,一个强联通分量里的牛都是互相喜欢的(滑稽脸。当缩完点后,可以发现所有奶牛块的喜欢都会有传递性的,最后那个被传递到的奶牛块就是明星奶牛块,也就是出度为0的奶牛块是明星奶牛块。但要是有多个出度为 \(0\) 的奶牛块,那么就不存在明星奶牛块了。
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
int n,m;
vector <int> e[N];
vector <int> e2[N];
int f[N];
int tol;
int ans;
int dfn[N],low[N],col[N],cnt,tme;
int sum[N],ind[N];
stack <int> s;
void tarjan(int x)
{
s.push(x);
dfn[x]=low[x]=++tme;
int _size=e[x].size();
for (int i=0;i<_size;i++)
{
int v=e[x][i];
if (!dfn[v]) tarjan(v),low[x]=min(low[x],low[v]);
else if (!col[v]) low[x]=min(low[x],low[v]);
}
if (dfn[x]==low[x])
{
col[x]=++cnt;
sum[cnt]++;
while (s.top()!=x)
{
sum[cnt]++;
col[s.top()]=cnt;
s.pop();
}
s.pop();
}
}
void build()
{
for (int i=1;i<=n;i++)
{
int _size=e[i].size();
for (int j=0;j<_size;j++)
{
int v=e[i][j];
if (col[i]!=col[v]) e2[col[i]].push_back(col[v]),ind[col[i]]++;
}
}
}
void topSort()
{
queue <int> q;
for (int i=1;i<=cnt;i++)
{
if (!ind[i]) q.push(i);
f[i]=sum[i];
}
while (!q.empty())
{
int u=q.front();
q.pop();
int _size=e2[u].size();
for (int i=0;i<_size;i++)
{
int v=e2[u][i];
f[v]+=f[u];
ind[v]--;
if (!ind[v]) q.push(v);
}
}
}
signed main()
{
scanf("%lld%lld",&n,&m);
for (int i=1,a,b;i<=m;i++)
{
scanf("%lld%lld",&a,&b);
e[a].push_back(b);
}
for (int i=1;i<=n;i++)
{
if (!dfn[i]) tarjan(i);
}
build();
for (int i=1;i<=cnt;i++)
{
if (!ind[i]) ans+=sum[i],tol++;
if (tol>1) { printf("0");return 0; }
}
printf("%lld",ans);
return 0;
}
C.最大半连通子图
史一般的题目叙述翻译题意后,发现所谓半连通就是一条链上串着几个环,要求的就是最长链的长度以及个数。所以缩点之后跑最长链即可,注意重边。
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,m,x;
vector <int> e[N];
vector <int> e2[N];
int g[N],f[N];
int ans1,ans2;
int klee[N];
int dfn[N],low[N],col[N],tme;
int sum[N],cnt;
int vis[N];
stack <int> st;
void tarjan(int x)
{
st.push(x);
dfn[x]=low[x]=++tme;
int _size=e[x].size();
for (int i=0;i<_size;i++)
{
int v=e[x][i];
if (!dfn[v]) tarjan(v),low[x]=min(low[x],low[v]);
else if (!col[v]) low[x]=min(low[x],dfn[v]);
}
if (dfn[x]==low[x])
{
col[x]=++cnt;
sum[cnt]++;
while (st.top()!=x)
{
col[st.top()]=cnt;
sum[cnt]++;
st.pop();
}
st.pop();
}
}
void build()
{
for (int i=1;i<=n;i++)
{
g[i]=1;
f[i]=sum[i];
int _size=e[i].size();
for (int j=0;j<_size;j++)
{
int v=e[i][j];
if (col[i]!=col[v]) e2[col[i]].push_back(col[v]);
}
}
}
int used[N];
void topSort()
{
for (int i=cnt;i>=1;i--)
{
int _size=e2[i].size();
for (int j=0;j<_size;j++)
{
int v=e2[i][j];
if (used[v]==i) continue;
used[v]=i;
if (f[v]<f[i]+sum[v])
{
f[v]=f[i]+sum[v];
g[v]=g[i];
}
else if (f[v]==f[i]+sum[v]) g[v]=(g[v]+g[i])%x;
//ans1=max(ans1,f[v]);
}
}
}
signed main()
{
scanf("%lld%lld%lld",&n,&m,&x);
for (int i=1,a,b;i<=m;i++)
{
scanf("%lld%lld",&a,&b);
e[a].push_back(b);
}
for (int i=1;i<=n;i++)
{
if (!dfn[i]) tarjan(i);
}
build();
topSort();
for (int i=1;i<=cnt;i++)
{
if (ans1<f[i])
{
ans1=f[i];
ans2=g[i];
}
else if (f[i]==ans1) ans2=(ans2+g[i])%x;
}
printf("%lld\n%lld",ans1,ans2);
return 0;
}
D.恒星的亮度
放个差分约束小链接
对于题中给出的所有关系,都可以简化为 \(a_{i}+b\leqslant a_{j}\),所以就可以从 \(i\) 向 \(j\) 连一条权值为 \(b\) 的边。因为一个强联通分量中的所有点权应该相同,若其中存在点权为 \(1\) 的点,那么一定无解。
缩完点后,跑一遍拓扑序更新并记录点权和即可。
贴
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,m;
struct node { int nxt,val; };
vector <node> e[N];
vector <node> e2[N];
int c[N],num[N];
int ind[N];
int ans;
int dfn[N],low[N],tme,col[N],cnt;
stack <int> st;
void tarjan(int x)
{
dfn[x]=low[x]=++tme;
st.push(x);
int _size=e[x].size();
for (int i=0;i<_size;i++)
{
int v=e[x][i].nxt;
if (!dfn[v]) tarjan(v),low[x]=min(low[x],low[v]);
else if (!col[v]) low[x]=min(low[x],dfn[v]);
}
if (dfn[x]==low[x])
{
col[x]=++cnt;
num[cnt]++;
while (st.top()!=x)
{
col[st.top()]=cnt;
num[cnt]++;
st.pop();
}
st.pop(),c[cnt]=1;
}
}
void topS()
{
queue <int> q;
for (int i=1;i<=cnt;i++)
{
if (ind[i]==0) q.push(i);
}
while (!q.empty())
{
int t=q.front();
q.pop();
ans+=c[t]*num[t];
int _size=e2[t].size();
for (int i=0;i<_size;i++)
{
int v=e2[t][i].nxt;
c[v]=max(c[v],c[t]+e2[t][i].val);
ind[v]--;
if (!ind[v]) q.push(v);
}
}
}
signed main()
{
scanf("%lld%lld",&n,&m);
for (int i=1,t,a,b;i<=m;i++)
{
scanf("%lld%lld%lld",&t,&a,&b);
switch (t)
{
case 1:e[a].push_back({b,0});e[b].push_back({a,0});break;
case 2:e[a].push_back({b,1});break;
case 3:e[b].push_back({a,0});break;
case 4:e[b].push_back({a,1});break;
case 5:e[a].push_back({b,0});break;
}
}
for (int i=1;i<=n;i++) if (!dfn[i]) tarjan(i);
for (int i=1;i<=n;i++)
{
int _size=e[i].size();
for (int j=0;j<_size;j++)
{
int v=e[i][j].nxt;
if (col[i]!=col[v]) e2[col[i]].push_back({col[v],e[i][j].val}),ind[col[v]]++;
else if (e[i][j].val==1) { printf("-1"); return 0; }
}
}
topS() , printf("%lld",ans);
return 0;
}
E.网络传输
缩点,缩点后强连通分量中的权值就归零了,跑一遍最短路即可。
贴
#incIude <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=2e5+5;
int n,m;
struct node{
int nxt,val;
};
vector <node> e[N];
vector <node> e2[N];
int dfn[N],low[N],tme,sum[N],cnt,col[N];
stack <int> s;
void tarjan(int x)
{
s.push(x);
dfn[x]=low[x]=++tme;
int _size=e[x].size();
for (int i=0;i<_size;i++)
{
int v=e[x][i].nxt;
if (!dfn[v]) tarjan(v),low[x]=min(low[x],low[v]);
else if (!col[v]) low[x]=min(low[x],dfn[v]);
}
if (dfn[x]==low[x])
{
col[x]=++cnt;
while (s.top()!=x)
{
col[s.top()]=cnt;
s.pop();
}
s.pop();
}
}
priority_queue < pii,vector <pii>,greater<pii> > q;
int dis[N];
int vis[N];
int dijkstra()
{
memset(dis,0x3f,sizeof dis);
dis[col[1]]=0;
q.push(make_pair(0,col[1]));
while (!q.empty())
{
int u=q.top().second;
q.pop();
if (vis[u]) continue;
vis[u]=1;
int _size=e2[u].size();
for (int i=0;i<_size;i++)
{
int v=e2[u][i].nxt,w=e2[u][i].val;
if (dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
q.push(make_pair(dis[v],v));
}
}
}
return dis[col[n]];
}
signed main()
{
scanf("%lld%lld",&n,&m);
for (int i=1,u,v,w;i<=m;i++)
{
scanf("%lld%lld%lld",&u,&v,&w);
e[u].push_back((node){v,w});
}
for (int i=1;i<=n;i++)
{
if (!dfn[i]) tarjan(i);
}
for (int i=1;i<=n;i++)
{
int _size=e[i].size();
for (int j=0;j<_size;j++)
{
int v=e[i][j].nxt,w=e[i][j].val;
if (col[i]!=col[v]) e2[col[i]].push_back((node){col[v],w});
}
}
printf("%lld",dijkstra());
return 0;
}
F.通讯问题
先缩点,缩完点后贪心构造方案(是没法跑MST的):对于点\(u\),一定由它相连边权最小的边转移而来,于是都不用建图,处理出所有点转移的最小边权相加就是答案。
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+5;
int n,m;
struct node{
int nxt,val;
};
vector <node> e[N];
vector <node> e2[N];
int sum[N];
int ans;
int dfn[N],low[N],cnt,tme,num[N],col[N];
stack <int> st;
void init()
{
memset(dfn,0,sizeof dfn);
memset(low,0,sizeof low);
memset(num,0,sizeof num);
memset(col,0,sizeof col);
memset(sum,0x3f,sizeof sum);
cnt=tme=ans=0;
for (int i=0;i<N;i++) e[i].clear(),e2[i].clear();
}
void tarjan(int x)
{
dfn[x]=low[x]=++tme;
st.push(x);
int _size=e[x].size();
for (int i=0;i<_size;i++)
{
int v=e[x][i].nxt;
if (!dfn[v]) tarjan(v),low[x]=min(low[x],low[v]);
else if (!col[v]) low[x]=min(low[x],dfn[v]);
}
if (dfn[x]==low[x])
{
col[x]=++cnt;
num[cnt]++;
while (st.top()!=x)
{
col[st.top()]=cnt;
num[cnt]++;
st.pop();
}
st.pop();
}
}
signed main()
{
while (scanf("%lld%lld",&n,&m))
{
if (n==0&&m==0) break;
init();
for (int i=1,x,y,c;i<=m;i++)
{
scanf("%lld%lld%lld",&x,&y,&c);
e[x].push_back({y,c});
}
for (int i=0;i<n;i++)
{
if (!dfn[i]) tarjan(i);
}
for (int i=0;i<n;i++)
{
int _size=e[i].size();
for (int j=0;j<_size;j++)
{
int v=e[i][j].nxt;
if (col[i]!=col[v]) sum[col[v]]=min(sum[col[v]],e[i][j].val);
}
}
sum[col[0]]=0;
for (int i=1;i<=cnt;i++) ans+=sum[i];
printf("%lld\n",ans);
}
return 0;
}
G.删点次数
在同一个scc内的点不能同时取,在同一条链上的点也不能同时取。所以,在缩点之后跑一遍最长链,最长链上的节点数量就是答案。
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
int n,m;
vector <int> e1[N];
vector <int> e2[N];
stack <int> st;
int dfn[N],low[N],tme,col[N],cnt,num[N];
int ind[N],f[N];
int ans;
void tarjan(int x)
{
dfn[x]=low[x]=++tme;
st.push(x);
int _size=e1[x].size();
for (int i=0;i<_size;i++)
{
int v=e1[x][i];
if (!dfn[v]) tarjan(v),low[x]=min(low[x],low[v]);
else if (!col[v]) low[x]=min(low[x],dfn[v]);
}
if (dfn[x]==low[x])
{
col[x]=++cnt;
while (st.top()!=x)
{
col[st.top()]=cnt,num[cnt]++;
st.pop();
}
f[cnt]=++num[cnt],st.pop();
}
}
void topS()
{
queue <int> q;
for (int i=1;i<=n;i++) if (!ind[i]) q.push(i);
while (!q.empty())
{
int t=q.front();
q.pop();
ans=max(ans,f[t]);
int _size=e2[t].size();
for (int i=0;i<_size;i++)
{
int v=e2[t][i];
f[v]=max(f[v],f[t]+num[v]);
ind[v]--;
if (!ind[v]) q.push(v);
}
}
}
signed main()
{
scanf("%lld%lld",&n,&m);
for (int i=1,u,v;i<=m;i++)
{
scanf("%lld%lld",&u,&v);
e1[u].push_back(v);
}
for (int i=1;i<=n;i++) if (!dfn[i]) tarjan(i);
for (int i=1;i<=n;i++)
{
int _size=e1[i].size();
for (int j=0;j<_size;j++)
{
int v=e1[i][j];
if (col[i]!=col[v]) e2[col[i]].push_back(col[v]),ind[col[v]]++;
}
}
topS(),printf("%lld",ans);
return 0;
}
H.软件安装
显然可以把依赖关系转化为一个图。处于一个强联通分量的所有点要么全选,要么不选。这样,再看一下数据范围,缩完点后跑一遍树形dp即可。
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=510;
int n,m;
int w[N],v[N],d[N];
struct node{
int nxt,val;
};
vector <int> e1[N];
vector <int> e2[N];
int cnt,_w[N],_v[N],ind[N];
int f[N][N];
int dfn[N],low[N],col[N],tme;
stack <int> st;
void tarjan(int x)
{
st.push(x);
dfn[x]=low[x]=++tme;
int _size=e1[x].size();
for (int i=0;i<_size;i++)
{
int v=e1[x][i];
if (!dfn[v]) tarjan(v),low[x]=min(low[x],low[v]);
else if (!col[v]) low[x]=min(low[x],dfn[v]);
}
if (dfn[x]==low[x])
{
col[x]=++cnt;
_w[cnt]=w[x],_v[cnt]=v[x];
while (st.top()!=x)
{
col[st.top()]=cnt;
_w[cnt]+=w[st.top()],_v[cnt]+=v[st.top()];
st.pop();
}
st.pop();
}
}
void dfs(int x)
{
int _size=e2[x].size();
for (int i=0;i<_size;i++)
{
int v=e2[x][i];
dfs(v);
for (int j=m;j>=_w[x];j--)
{
for (int k=_w[v];k<=j-_w[x];k++) f[x][j]=max(f[x][j],f[v][k]+f[x][j-k]);
}
}
}
signed main()
{
scanf("%lld%lld",&n,&m);
for (int i=1;i<=n;i++) scanf("%lld",&w[i]);
for (int i=1;i<=n;i++) scanf("%lld",&v[i]);
for (int i=1;i<=n;i++)
{
scanf("%lld",&d[i]);
e1[d[i]].push_back(i);
}
for (int i=1;i<=n;i++) if (!dfn[i]) tarjan(i);
for (int i=1;i<=n;i++)
{
int _size=e1[i].size();
for (int j=0;j<_size;j++)
{
int v=e1[i][j];
if (col[i]!=col[v])
{
e2[col[i]].push_back(col[v]);
ind[col[v]]++;
}
}
}
for (int i=1;i<=cnt;i++) if (!ind[i]) e2[0].push_back(i);
for (int i=1;i<=cnt;i++) f[i][_w[i]]=_v[i];
dfs(0);
printf("%lld",f[0][m]);
return 0;
}
I.宫室宝藏
显然要建图然后跑 \(tarjan\) 缩点然后 \(dp\),但是这样的话时间复杂度和空间复杂度可能会很爆炸,于是乎我们对于每一行、每一列都建一个虚点,虚点与该行/该列内的宝藏连通,这样就非常好了
我们令 \(x\in [1,r]\) 表示行的虚点,\(y\in [r+1,r+c]\) 表示列的虚点,\(k\in [r+c+1,r+c+n]\) 表示具体的点,每次操作分别连边建图跑 \(tarjan\)即可。
亲测vector爆炸
贴
#incIude <bits/stdc++.h>
#define pii pair<int,int>
using namespace std;
const int N=1e7+5;
int n,r,c;
int cnt;
struct NODE{
int to,nxt;
}e1[N],e2[N];
map <pii,int> mp;
struct node { int x,y,id; }a[N];
int dx[8]={-1,-1,-1,0,0,1,1,1},dy[8]={-1,0,1,-1,1,-1,0,1};
int sum[N],f[N],ans;
int cnt1,cnt2,head[N],head1[N];
void add(int u,int v) { e1[++cnt1]={v,head[u]}; head[u]=cnt1; }
void add2(int u,int v) { e2[++cnt2]={v,head1[u]}; head1[u]=cnt2; }
int dfn[N],col[N],low[N],tme;
stack <int> st;
void tarjan(int x)
{
dfn[x]=low[x]=++tme;
st.push(x);
for (int i=head[x];i;i=e1[i].nxt)
{
int v=e1[i].to;
if (!dfn[v]) tarjan(v),low[x]=min(low[x],low[v]);
else if (!col[v]) low[x]=min(low[x],dfn[v]);
}
if (dfn[x]==low[x])
{
col[x]=++cnt;
while (st.top()!=x)
{
col[st.top()]=cnt,sum[cnt]+=(st.top()>r+c);
st.pop();
}
sum[cnt]+=(st.top()>r+c),st.pop();
}
}
int main()
{
scanf("%d%d%d",&n,&r,&c);
for (int i=1,x,y,t;i<=n;i++)
{
scanf("%d%d%d",&x,&y,&t);
num[i]=1;
mp[make_pair(x,y)]=i;
add(x,i+r+c),add(y+r,i+r+c);
if (t==1) add(i+r+c,x);
else if(t==2) add(i+r+c,y+r);
else a[++cnt]={x,y,i};
}
for (int i=1;i<=cnt;i++)
{
for (int j=0;j<8;j++)
{
int xx=a[i].x+dx[j],yy=a[i].y+dy[j];
if (xx>=1&&xx<=r&&yy>=1&&yy<=c&&mp[make_pair(xx,yy)])
add(a[i].id+r+c,mp[make_pair(xx,yy)]+r+c);
}
}
cnt=0;
for (int i=1;i<=n+r+c;i++) if (!dfn[i]) tarjan(i);
for (int i=1;i<=n+r+c;i++)
{
for (int j=head[i];j;j=e1[j].nxt)
{
int v=e1[j].to;
if (col[i]!=col[v]) add2(col[i],col[v]);
}
}
for (int i=1;i<=cnt;i++) f[i]=sum[i];
for (int i=cnt;i>=1;i--)
{
for (int j=head1[i];j;j=e2[j].nxt)
{
int v=e2[j].to;
f[v]=max(f[v],f[i]+sum[v]);
}
}
for (int i=1;i<=cnt;i++) ans=max(ans,f[i]);
printf("%d",ans);
return 0;
}