Tarjan算法用于求一个有向图里的强连通分量有哪些?经常用于在有环图里把环压成一个点,在新图里求答案(依赖于一个点也是一个连通分量这个原因),实现战略上无环,毕竟环一定是强连通分量。
维基 https://zh.wikipedia.org/wiki/Tarjan%E7%AE%97%E6%B3%95
hihocoder http://hihocoder.com/problemset/problem/1185
博客 https://www.byvoid.com/blog/scc-tarjan
Tarjan还可以用于求割边割点:hihocoder题目、代码;LCA(看这里)
题意:有n头牛,每个牛都有崇拜的牛,问有多少个牛被其他所有牛都崇拜。
做法:显然就是求出度为0的牛(点)个数,但有环!所以用Tarjan把环缩成点,在新图里求出度为0的分量数,其中度为0的分量数如果大于1则显然不会相互他们崇拜,否则度为0的分量里有多少个牛那么这些牛都是被其他牛崇拜的(因为整体被其他分量崇拜,而内部又是环)
#include
#include
#include
#include
#include
#include
using namespace std;
#define clr( a , x ) memset ( a , x , sizeof (a) );
#define RE freopen("1.in","r",stdin);
#define WE freopen("1.out","w",stdout);
const int maxn = 10005;
const int maxm = 50005;
int head[maxn];
int eCnt;
struct Edge
{
int v, next;
} edge[maxm];
void add(int u, int v) {
edge[eCnt].v = v, edge[eCnt].next = head[u], head[u] = eCnt++;
}
int dfn[maxn], low[maxn];
int vis[maxn], inStack[maxn], _stack[maxn];
int rtCnt,rt[maxn]; //强连通分量数目、所在的强连通分量的序号
int idx,top; //遍历顺序编号、栈索引
void Tarjan(int u) {
dfn[u] = low[u] = idx++;
vis[u] = inStack[u] = 1;
_stack[top++] = u;
for (int i = head[u]; ~i; i = edge[i].next) {
int v = edge[i].v;
if (!vis[v]) {
Tarjan(v);
low[u] = min(low[u], low[v]);
} else if (inStack[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u]) {
rtCnt++;
while (top > 0 && _stack[top] != u) {
top--;
int v = _stack[top];
rt[v] = rtCnt; //同属第rtCnt个分量
inStack[v] = 0;
}
}
}
void init(){
top = idx = 1;
rtCnt = 0;
clr(_stack, 0);
clr(inStack, 0);
clr(vis, 0);
clr(head, -1);
}
int du[maxn];
int main() {
int n, from,to,m;
while (~scanf("%d%d",&n,&m)) {
init();
for (int i = 1; i <= m; ++i) {
scanf("%d%d",&from,&to);
add(from,to);
}
for (int i = 1; i <= n; ++i) {
if (!vis[i]) {
Tarjan(i);
}
}
clr(du,0);
for (int u = 1; u <= n; ++u) {
for (int i = head[u]; ~i; i = edge[i].next) {
if (rt[edge[i].v] != rt[u]) {
du[rt[u]]++;
}
}
}
int ansRt;
int zero = 0;
for (int i = 1; i <= rtCnt; ++i) {
if(!du[i]){
ansRt = i;
zero++;
}
}//出度为0的分量里有多少个牛,计数
if(zero == 1){
int ans = 0;
for (int u = 1; u <= n; ++u){
if(rt[u] == ansRt){
ans++;
}
}
printf("%d\n", ans);
}else{//出度为0的分量>1则不会有相互崇拜的情况
printf("0\n");
}
}
return 0;
}
题意:一个包含1-n号学校的网络,每个学校有个软件分发列表,当学校拿到软件时会把软件分发给列表里的学校。
问1:一个新软件出现时初始化情况至少需要给多少个学校才能让它到达整个网络?
问2:至少需要添加多少个名单才能使从任意一个学校开始分发都能充满整个网络?
转成图的理解就是↓,引用kuangbin博客
也就是:
—给定一个有向图,求:1) 至少要选几个顶点,才能做到从这些顶点出发,可以到达全部顶点
2) 至少要加多少条边,才能使得从任何一个顶点出发,都能到达全部顶点
— 顶点数<= 100
解题思路:
1. 求出所有强连通分量
2. 每个强连通分量缩成一点,则形成一个有向无环图DAG。
3. DAG上面有多少个入度为0的顶点,问题1的答案就是多少
在DAG上要加几条边,才能使得DAG变成强连通的,问题2的答案就是多少
加边的方法:
要为每个入度为0的点添加入边,为每个出度为0的点添加出边
假定有 n 个入度为0的点,m个出度为0的点,如何加边?
把所有入度为0的点编号 0,1,2,3,4 ….N -1
每次为一个编号为i的入度0点可达的出度0点,添加一条出边,连到编号为(i+1)%N 的那个出度0点,
这需要加n条边
若 m <= n,则
加了这n条边后,已经没有入度0点,则问题解决,一共加了n条边
若 m > n,则还有m-n个入度0点,则从这些点以外任取一点,和这些点都连上边,即可,这还需加m-n条边。
所以,max(m,n)就是第二个问题的解
此外:当只有一个强连通分支的时候,就是缩点后只有一个点,虽然入度出度为0的都有一个,但是实际上不需要增加清单的项了,所以答案是1,0;
Code:
#include
#include
#include
#include
#include
#include
using namespace std;
#define ll long long
#define clr( a , x ) memset ( a , x , sizeof (a) );
#define RE freopen("1.in","r",stdin);
#define WE freopen("1.out","w",stdout);
#define SpeedUp std::cout.sync_with_stdio(false);
const int maxn = 105;
const int maxm = maxn * (maxn - 1) / 2;
int head[maxn];
int eCnt;
struct Edge
{
int v, next;
} edge[maxm];
void add(int u, int v) {
edge[eCnt].v = v, edge[eCnt].next = head[u], head[u] = eCnt++;
}
int dfn[maxn], low[maxn];
int vis[maxn], inStack[maxn], _stack[maxn];
int rt[maxn]; //所在的强连通分量的序号
int rtCnt; //强连通分量数目
int idx; //遍历顺序编号
int top; //栈索引
void Tarjan(int u) {
dfn[u] = low[u] = idx++;
vis[u] = inStack[u] = 1;
_stack[top++] = u;
for (int i = head[u]; ~i; i = edge[i].next) {
int v = edge[i].v;
if (!vis[v]) {
Tarjan(v);
low[u] = min(low[u], low[v]);
} else if (inStack[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u]) { //连通分量根
rtCnt++;
while (top > 0 && _stack[top] != u) {
top--;
int v = _stack[top];
rt[v] = rtCnt; //赋值为u的话就成并查集了
inStack[v] = 0;
}
}
}
int ind[maxn], outd[maxn]; //强连通分量的出入度
int main() {
int n, to;
// RE
while (cin >> n) {
top = idx = 1;
rtCnt = 0;
clr(_stack, 0);
clr(inStack, 0);
clr(vis, 0);
clr(head, -1);
// clr(dfn,0);clr(low,0);clr(rt,0); //没必要
for (int i = 1; i <= n; ++i) {
cin >> to;
while (to) {
add(i, to);
cin >> to;
}
}
for (int i = 1; i <= n; ++i) {
if (!vis[i]) {
Tarjan(i);
}
}
//遍历每个点的每条边,如果两顶点不是一个分量里的则统计出入度
//若结构体edge也存起点的话可以直接遍历边数
clr(ind, 0);
clr(outd, 0);
for (int u = 1; u <= n; ++u) {
for (int i = head[u]; ~i; i = edge[i].next) {
if (rt[edge[i].v] != rt[u]) {
ind[rt[edge[i].v]]++;
outd[rt[u]]++;
}
}
}
//ans1 = 入度为0的强联通分量,ans2 = max(入度0分量数,出度0分量数)
int cntIn = 0, cntOut = 0;
for (int i = 1; i <= rtCnt; ++i) {
if (!ind[i])
cntIn++;
if (!outd[i])
cntOut++;
}
cout << cntIn << endl;
if (rtCnt == 1) //一个分量时不需要补充边
cout << "0" << endl;
else
cout << max(cntIn, cntOut) << endl;
}
return 0;
}
题意:n个王子n个女孩,每个王子喜欢一些编号的女孩,问在确保每个王子能找到女孩的情况下每个王子各能选择哪些女孩?题目还给了一个完美匹配的选择情况当已知。
做法:若from王子喜欢to女孩,则from连边到to;题目给的匹配是form对应to女孩,则把to连向from,则每个王子都能选择跟他在一个连通分量里的女孩。为啥?证明看这里
注意下用scanf代替cin
#include
#include
#include
#include
#include
#include
using namespace std;
#define clr( a , x ) memset ( a , x , sizeof (a) );
#define RE freopen("1.in","r",stdin);
const int maxn = 4005;
const int maxm = 400005;
int head[maxn];
int eCnt;
struct Edge
{
int v, next;
} edge[maxm];
void add(int u, int v) {
edge[eCnt].v = v, edge[eCnt].next = head[u], head[u] = eCnt++;
}
int dfn[maxn], low[maxn];
int vis[maxn], inStack[maxn], _stack[maxn];
int rtCnt,rt[maxn]; //强连通分量数目、所在的强连通分量的序号
int idx,top; //遍历顺序编号、栈索引
void Tarjan(int u) {
dfn[u] = low[u] = idx++;
vis[u] = inStack[u] = 1;
_stack[top++] = u;
for (int i = head[u]; ~i; i = edge[i].next) {
int v = edge[i].v;
if (!vis[v]) {
Tarjan(v);
low[u] = min(low[u], low[v]);
} else if (inStack[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u]) { //连通分量根
rtCnt++;
while (top > 0 && _stack[top] != u) {
top--;
int v = _stack[top];
rt[v] = rtCnt; //赋值为u的话就成并查集了
inStack[v] = 0;
}
}
}
void init(){
top = idx = 1;
rtCnt = 0;
clr(_stack, 0);
clr(inStack, 0);
clr(vis, 0);
clr(head, -1);
// clr(dfn,0);clr(low,0);clr(rt,0);
}
int ans[maxn];
int main() {
int n, to;
// RE
while (~scanf("%d",&n)) {
init();
int mmm;
for (int i = 1; i <= n; ++i) {
scanf("%d",&mmm);
while (mmm--) {
scanf("%d",&to);
add(i, n+to);
}
}
for (int i = 1; i <= n; ++i) {
scanf("%d",&to);
add(n+to,i);
}
for (int i = 1; i <= 2*n; ++i) { //~
if (!vis[i]) {
Tarjan(i);
}
}
for (int u = 1; u <= n; ++u) {
int k = 0;
for (int i = head[u]; ~i; i = edge[i].next) {
if (rt[edge[i].v] == rt[u]) {
ans[k++] = edge[i].v - n;
}
}
sort(ans,ans+k);
printf("%d",k );
for (int i = 0; i < k; ++i) {
printf(" %d", ans[i]);
}
printf("\n");
}
}
return 0;
}
LCA问题,见此博另一篇文章
#include
#include
#include
#include
#include
#include
using namespace std;
#define ll long long
#define clr( a , x ) memset ( a , x , sizeof (a) );
#define RE freopen("1.in","r",stdin);
#define WE freopen("1.out","w",stdout);
#define SpeedUp std::cout.sync_with_stdio(false);
#define debug(x) cout << "Line " << __LINE__ << ": " << #x << " = " << x << endl;
const int maxn = 10005;
const int maxm = 10005;
const int inf = 0x3f3f3f3f;
const int maxq = 1;
int eCnt;
int head[maxn];
struct Edge
{
int v, next;
} edge[maxm];
void addEdge(int u, int v) {
edge[eCnt].v = v, edge[eCnt].next = head[u]; head[u] = eCnt++;
}
//---离线处理用到的查询
int ans[maxq];
int qCnt;
int qHead[maxn];
struct Query
{
int v,next;
int index; //查询的编号
}query[maxq*2];
void addQuery(int u, int v,int index) { //双向,因为查u,v和查v,u是一样的
query[qCnt].index = index;
query[qCnt].v = v, query[qCnt].next = qHead[u]; qHead[u] = qCnt++;
query[qCnt].index = index;
query[qCnt].v = u, query[qCnt].next = qHead[v]; qHead[v] = qCnt++;
}
//---并查集
int father[maxn];
int find(int x){
if(x != father[x]){
father[x] = find(father[x]);
}
return father[x];
}
void merge(int x,int y){
x = find(x);
y = find(y);
if(x != y)
father[x] = y;
}
int ancestor[maxn];
int du[maxn];
int vis[maxn];
void init(){
clr(head,-1);
clr(qHead,-1);
clr(vis,0);
eCnt = 0;
qCnt = 0;
clr(du,0);
}
void lca(int u){
ancestor[u] = u;
vis[u] = 1;
//遍历u点的邻接边
for (int i = head[u]; ~i; i = edge[i].next){
int v = edge[i].v;
if(vis[v]){
continue;
}
lca(v);
merge(u,v);
ancestor[find(v)] = u;
}
//找跟当前u点有关的查询,这个uv查询的答案是v点所在集合的祖先
for (int i = qHead[u]; ~i; i = query[i].next){
int v = query[i].v;
if(vis[v]){
ans[query[i].index] = ancestor[find(v)];
}
}
}
int main() {
// RE
int t;
int n,u,v;
while(scanf("%d",&t)!=EOF){
while(t--){
init();
scanf("%d",&n);
for (int i = 1; i <= n-1; ++i){
scanf("%d%d",&u,&v);
addEdge(u,v);
du[v]++;
father[i] = i;
}
father[n] = n;
int queryCnt = 1; //查询次数
for (int i = 1; i <= queryCnt; ++i){
scanf("%d%d",&u,&v);
addQuery(u,v,i);
}
//找入度为0的点当树根
int root = 1;
for (int i = 1; i <= n; ++i){
if(!du[i]){
root = i;
break;
}
}
lca(root);
for (int i = 1; i <= queryCnt; ++i){
printf("%d\n", ans[i]);
}
}
}
return 0;
}