tarjan有向图
有向图强连通分量SCC
强连通分量指的是:在一个有向图中,强联通分量的点可以互相到达
有向图中,如果同时存在从\(x\)到\(y\)和从\(y\)到\(x\)的有向路径,则称x和y强连通。
有向图的极大强连通子图为强连通分量
图中的每个点只会属于一个强联通分量
孤立的一个点也是一个强连通分量
tarjan算法
时间复杂度是\(O(N+M)\)
原理是\(dfs\)求出图的任意生成树,利用返祖边找环。(忘了返祖边树边横叉边的戳这里)
对一个点维护\(dfn[x]\)时间戳表示该点的\(dfs\)序,\(low[x]\)表示 \(x\)不经过\(x\)的父节点(也就是走返祖边)能到达的最浅的祖先
遍历完\(x\)的子树后,若 \(dfn[x]==low[x]\)则将当前栈中\(x\)以上的所有点和\(x\)形成一个强联通分量
缩点模板
tarjan缩点 + DAGdp
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N=2e5+5;
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 f*x;
}
int n,m;
int hd[N],to[N<<1],tot,nxt[N<<1];
inline void add(int x,int y){
to[++tot]=y;nxt[tot]=hd[x];hd[x]=tot;
}
int st[N],tp,dfn[N],low[N];
int sum[N],dcc,col[N],val[N],cnt;
bool vis[N];
void tarjan(int x) {
vis[x]=1;
st[++tp]=x;
dfn[x]=low[x]=++cnt;
for(int i=hd[x];i;i=nxt[i]) {
int y=to[i];
if(!dfn[y]) {
tarjan(y);
low[x]=min(low[x],low[y]);
}
else if(vis[y])
low[x]=min(low[x],dfn[y]);
}
if(dfn[x]==low[x]) {
dcc++;
while(st[tp+1]!=x) {
col[st[tp]]=dcc;
sum[dcc]+=val[st[tp]];
vis[st[tp--]]=0;
}
}
}
int u[N],v[N];
vector<int>G[N];
int f[N];
void dp(int x) {
if(f[x]) return;
f[x]=sum[x];
int res=0;
for(unsigned int i=0;i<G[x].size();i++) {
int y=G[x][i];
if(!f[y]) dp(y);
res=max(res,f[y]);
}
f[x]+=res;
}
int ans;
int main() {
n=read();m=read();
for(int i=1;i<=n;i++) val[i]=read();
for(int i=1;i<=m;i++) {
u[i]=read(),v[i]=read();
add(u[i],v[i]);
}
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i);
for(int i=1;i<=m;i++)
if(col[u[i]]!=col[v[i]])
G[col[u[i]]].push_back(col[v[i]]);
for(int i=1;i<=dcc;i++) {
dp(i);
ans=max(ans,f[i]);
}
printf("%d",ans);
return 0;
缩点水题:
The Cow Prom S
\(n\)个点,\(m\)条边的有向图,请求出这个图点数大于 1的强联通分量个数
消息扩散
缩点。。。then 一个强连通分量的只用1个就行,用个入度统计强连通分量
受欢迎的牛 G
受欢迎的奶牛只有可能是图中唯一的出度为零的强连通分量中的所有奶牛,所以若出现两个以上出度为0的强连通分量则不存在明星奶牛,因为那几个出度为零的分量的爱慕无法传递出去
for(int x=1;x<=n;x++)
for(int i=hd[x];i;i=nxt[i])
if(col[x]!=col[to[i]])
out[col[x]]++;
bool flag=0;
for(int i=1;i<=scc;i++) {
if(flag&&!out[i]) {puts("0");return 0;}
if(!out[i]) ans=cnt[i],flag=1;
}
校园网Network of Schools
数据加强版:校园网络
第一问简单,求一下无入度点个数即可。
第二问简化后问题是给一张DAG求最少添加几条边使得DAG变成一个SCC。我们其实只用关注出度为0的点,如果出度为0的点的个数小于等于入度为0的,那么直接出度为0的点向入度为0的点连边,ans=出度为0的点的个数,否则有些入度为0的点无法被到达,所以ans=入度为0的点的个数
网上题解:
二分图连边表示S点(入度为零)可以走到T点(出度为零),然后先暴力匹配,表示每一个 \(S_i\)尽可能走一个互不相同的 \(T_i\)点,然后所有匹配边从 \(T_i\)向下一个匹配的 \(S_{i+1}\)连一条边,表示从 \(S_i\to T_i\to S_{i+1}\),如此往复,最终将最后一个 \(T_k\)连向开始时的 \(S_1\),此时形成一个环,是SCC。然后剩下的没被选上的是不连通的。如果是 \(S_i\)点表明他所有可以走到的 \(T\)都被其他 \(S\)走过去了, \(T\)也是一样,能走到他的 \(S\)都走到其他点 \(T\)了,于是就把每一对未匹配的 \(T\)向 \(S\)连一下边,最后如果还剩下来就随便连了。这样,会发现左右两侧的点就都被连上了一条边。所以最少连边数量是 \(\max(|S|,|T|)\)。
注意特判scc=1,ans1=1,ans2=0;
刻录光盘
并查集可做,但wa了。。。原因是一般的并查集是双向的
有五个点1,2,3,4,5;
1给2拷贝,
2给4,5拷贝,
3给4拷贝,
4,5不给别人。
那么我们就会发现了,3的父亲会变成1。。。
所以我们记录入度为0的点,入度为0的点必须要给光盘,然后快乐AC
const int N=205;
const int M=8005;
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return f*x;
}
int n;
int in[N],ans;
int fa[N];
inline int find(int x) {
return x==fa[x]?x:fa[x]=find(fa[x]);
}
int main() {
n=read();
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1,x;i<=n;i++) {
while(1) {
x=read();
if(x==0) break;
in[x]++;
int fx=find(x),fy=find(i);
if(fx!=fy) fa[fx]=fy;
}
}
for(int i=1;i<=n;i++)
if(fa[i]==i||in[i]==0)
ans++;
printf("%d\n",ans);
return 0;
}
咳咳回归正题——tarjan
就缩点,求入度为0的个数 和例2一毛一样
以上皆为入门无脑题
小z玩游戏
暴力连边是\(O(N^2)\)的,考虑优化:
因为只能连倍数,所以我们可以提前把每个数和他的倍数连起来,线性筛
这样复杂度大概是\(O(nlogn)\),然后再$w[i] - > e[i] $连边
int prime[N],mindiv[N];
void init() {
for(int i=2;i<100000;i++) {
if(!mindiv[i]) mindiv[i]=i,prime[++prime[0]]=i;
for(int j=1;j<=prime[0]&&i*prime[j]<100000&&prime[j]<=mindiv[j];j++)
mindiv[i*prime[j]]=prime[j];
}
}
int T,n,w[N],e[N],ans;
int main() {
T=read();
init();
prime[++prime[0]]=110000;
while(T--) {
n=read();
for(int i=1;i<=n;i++) w[i]=read();
for(int i=1;i<=n;i++) e[i]=read(),add(w[i],e[i]);
for(int i=1;i<100000;i++)
for(int j=1;i*prime[j]<100000;j++)
add(i,i*prime[j]);
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i);
for(int i=1;i<=n;i++)
if(col[w[i]]==col[e[i]])
ans++;
printf("%d\n",ans);
clear();
}
return 0;
}
通讯
https://blog.csdn.net/sadnohappy/article/details/52192705
缩点+贪心(除起点外,入边权值最小)
int main() {
while(1) {
clear();
n=read();m=read();
if(n==0&&m==0) return 0;
for(int i=1;i<=m;i++) {
x[i]=read()+1;y[i]=read()+1;z[i]=read();
add(x[i],y[i]);
}
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i);
for(int i=1;i<=m;i++)
if(col[x[i]]!=col[y[i]])
Min(ans[col[y[i]]],z[i]);
long long sum=0;
for(int i=1;i<=scc;i++)
if(col[1]!=i)
sum+=ans[i];
printf("%lld\n",sum);
}
}
中山市选 杀人游戏
缩点
对于入度为0的强连通块,我们只需访问一次,答案就是联通块数量 ...吗?
注意排除法:如果有个强连通块大小为1,入度为0,且它能到达的点都有别的点访问了,那么这个点可以被排除,所以$ ans--$
#include <vector>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int M=600010;
const int N=500010;
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 f*x;
}
int n,m;
int hd[N],to[M],nxt[M],tot;
inline void add(int x,int y) {
to[++tot]=y;nxt[tot]=hd[x];hd[x]=tot;
}
int dfn[N],low[N],st[N],tp,col[N],cnt,dcc,sum[N];
bool vis[N];
void tarjan(int x) {
vis[x]=1;
st[++tp]=x;
dfn[x]=low[x]=++cnt;
for(int i=hd[x];i;i=nxt[i]) {
int y=to[i];
if(!dfn[y]) {
tarjan(y);
low[x]=min(low[x],low[y]);
} else if(vis[y])
low[x]=min(low[x],dfn[y]);
}
if(dfn[x]==low[x]) {
dcc++;
while(st[tp+1]!=x) {
col[st[tp]]=dcc;
sum[dcc]++;
vis[st[tp--]]=0;
}
}
}
vector<int>G[N];
int x[N],y[N],inq[N];
bool check(int x) {
for(auto y:G[x])
if(inq[y]==1)
return 0;
return 1;
}
int main() {
// freopen("8.in","r",stdin);
// freopen("my.out","w",stdout);
n=read();m=read();
for(int i=1;i<=m;i++) {
x[i]=read(),y[i]=read();
add(x[i],y[i]);
}
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i);
for(int i=1;i<=m;i++)
if(col[x[i]]!=col[y[i]]) {
G[col[x[i]]].push_back(col[y[i]]);
inq[col[y[i]]]++;
}
int ans=0;
bool flag=0;
for(int i=1;i<=dcc;i++) {
if(!inq[i]) {
ans++;
if(sum[i]==1&&check(i)==1&&!flag) flag=1;
}
}
if(flag) ans--;
printf("%.6lf\n",(double)(n-ans)/n);
return 0;
}
所驼门王的宝藏
很显然就是缩点连边,然后跑最长链(和缩点板子没啥区别)
连边的话,每一行找一个“ 横天门 ”,然后和这行的其他“ 横天门 ”连双向边,和别的连单向边,纵向亦然。
对于自由door就check检查一下就好——注意亿些些细节
垃圾洛谷卡空间,新图20000足够
#include <map>
#include <utility>
#include <vector>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N=1000010;
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 f*x;
}
inline void Max(int &x,int y){if(x<y)x=y;}
inline void Min(int &x,int y){if(x>y)x=y;}
int n,R,C;
#define pii pair<int,int>
#define MP make_pair
map< pii,int >mp;
map< pii,int >id;
vector< pii >H[N];//heng
vector< pii >Z[N];//zong
int to[N],tot,hd[N],nxt[N];
int X[N],Y[N],edge;
inline void add(int x,int y) {
if(x==0||y==0) return;
// cout<<x<<" "<<y<<'\n';
X[++edge]=x;Y[edge]=y;
to[++tot]=y;nxt[tot]=hd[x];hd[x]=tot;
}
inline void add2(int x,int y) {
if(x==0||y==0) return;
// cout<<x<<" "<<y<<'\n';
// cout<<y<<" "<<x<<'\n';
X[++edge]=x;Y[edge]=y;
X[++edge]=y;Y[edge]=x;
to[++tot]=y;nxt[tot]=hd[x];hd[x]=tot;
to[++tot]=x;nxt[tot]=hd[y];hd[y]=tot;
}
int dx[]={0,0,1,1,-1,-1,1,-1};
int dy[]={1,-1,1,-1,1,-1,0,0};
int num2;
void check(int p,pii a) {
int x=a.first;
for(int i=0;i<8;i++) {
if(p+dx[i]>R||p+dx[i]<1||x+dy[i]>C||x+dy[i]<1)
continue;
if(mp[MP(p+dx[i],x+dy[i])])
add(num2,id[MP(p+dx[i],x+dy[i])]);
}
}
void check1(int p,pii a) {
int x=a.first;
for(int i=0;i<8;i++) {
if(x+dx[i]>R||x+dx[i]<1||p+dy[i]>C||p+dy[i]<1)
continue;
if(mp[MP(x+dx[i],p+dy[i])])
add(num2,id[MP(x+dx[i],p+dy[i])]);
}
}
bool vis[N];
int cnt,dfn[N],low[N],dcc,st[N],tp,col[N],sum[500010];
void tarjan(int x) {
vis[st[++tp]=x]=1;
dfn[x]=low[x]=++cnt;
for(int i=hd[x];i;i=nxt[i]) {
int y=to[i];
if(!dfn[y]) {
tarjan(y);
Min(low[x],low[y]);
} else if(vis[y])
Min(low[x],dfn[y]);
}
if(dfn[x]==low[x]) {
++dcc;
while(st[tp+1]!=x) {
col[st[tp]]=dcc;
sum[dcc]++;
vis[st[tp--]]=0;
}
}
}
int f[200010];
vector<int>G[200010];
void dp(int x) {
if(f[x]) return;
f[x]=sum[x];
int mx=0;
for(auto y:G[x]) {
if(!f[y]) dp(y);
Max(mx,f[y]);
}
f[x]+=mx;
}
int main() {
n=read();R=read();C=read();
for(int i=1;i<=n;i++) {
int x=read(),y=read(),ty=read();
H[x].push_back(MP(y,ty));
Z[y].push_back(MP(x,ty));
id[MP(x,y)]=i;
mp[MP(x,y)]=ty;
}
for(int i=1;i<=R;i++) {
if(H[i].empty()) continue;
pii h;
for(auto x:H[i])
if(x.second==1) {h=x;break;}
int num=id[MP(i,h.first)];
for(auto x:H[i]) {
num2=id[MP(i,x.first)];
if(x.second==3)
check(i,x);
if(h==x) continue;
if(h.second==x.second) add2(num,num2);
else add(num,num2);
}
}
for(int i=1;i<=C;i++) {
if(Z[i].empty()) continue;
pii h;
for(auto x:Z[i])
if(x.second==2) {h=x;break;}
int num=id[MP(h.first,i)];
for(auto x:Z[i]) {
num2=id[MP(x.first,i)];
if(x.second==3)
check1(i,x);
if(h==x) continue;
if(h.second==x.second) add2(num,num2);
else add(num,num2);
}
}
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i);
for(int i=1;i<=edge;i++)
if(col[X[i]]!=col[Y[i]])
G[col[X[i]]].push_back(col[Y[i]]);
int ans=0;
for(int i=1;i<=dcc;i++) {
dp(i);
Max(ans,f[i]);
}
printf("%d\n",ans);
return 0;
}
Trick or Treat on the Farm G 缩点+dfs