tarjan复习笔记
tarjan复习笔记
(关于tarjan
读法,优雅一点读塔洋,接地气一点读塔尖)
0. 连通分量
有向图:
强连通分量(SCC)是个啥
就是一张图里面两个点能互相达到,那么这两个点在同一个强连通分量里,
极大强连通分量就是最大的强连通分量。
无向图:
一个全部联通的子图就是一个连通分量。
其中用到tarjan暂时还有边双连通分量(e-DCC)和点双连通分量(v-DCC)
边双连通分量(e-DCC)
指的是一个子图中没有桥的话,这就是一个边双连通分量。
一个无向图的每一个极大边双连通子图称作此无向图的双连通分量。
点双连通分量(v-DCC)
对于一个无向图,如果一个点集,它内部的任意一个点对之间,至少有两条点完全不重复的路径,那么这个点集就是原图的一个点双连通分量。
无向图的双连通分量下面待会复习
tarjan求强连通分量
这个就是所谓的tarjan强连通分量——缩点了。
对于一个单向联通的子图,我们从一个点出发会得到一个搜索树,
但是这个子图可不仅仅只有搜索树上的边。
(搜索树上的边下简称树边,非搜索树上的但是遇到的边下简称非树边)
按照搜索的顺序把点压到一个栈里。
如果当前我们找到一个非树边然后指向之前栈中的点里,那么从那个点到这个点之间都是强连通分量的一部分。
但是我们怎么找到一个最大的强连通分量呢?
我们设置一个搜索顺序dfn[]
和追溯值low[]
,让当前点的low[]
取到能够到的最靠上的点的dfn[]
或者自己的dfn[]
。
如果这个dfn[]!=low[]
,那么就是栈从当前点到下面的这些点都是在一个强连通分量里面。
可以证明这样求的一定是一个个最大的强连通分量。
代码实现(【模板】):
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=10086;
//forward star node
struct edge
{
int to,next;
} Edge[50086];
int cnt=1,head[maxn];
//forward star add edges
inline void add_edge(int from,int to)
{
Edge[cnt]=(edge){to,head[from]};
head[from]=cnt++;
}
//tarjan uses
int dfn[maxn],low[maxn],stack[maxn],now,top;
bool vst[maxn];
//After tarjan these point will put into baskets(group)
int group[maxn],gcnt;
void tarjan(int n)
{
dfn[n]=low[n]=++now;
stack[++top]=n;
vst[n]=true;
for (int i=head[n];i;i=Edge[i].next)
{
int to=Edge[i].to;
if (dfn[to]==0)
{
tarjan(to);
low[n]=min(low[n],low[to]);
}
else
{
if (vst[to])
{
low[n]=min(low[n],low[to]);
}
}
}
if (low[n]==dfn[n])
{
group[n]=++gcnt;
while (stack[top]!=n)
{
vst[stack[top]]=false;
group[stack[top--]]=gcnt;
}
//make n get out of stack
vst[n]=false;
top--;
}
}
int main()
{
int n,m;
cin>>n>>m;
for (register int i=1;i<=m;i++)
{
int f,t;
scanf("%d%d",&f,&t);
add_edge(f,t);
}
for (register int i=1;i<=n;i++)
{
if (dfn[i]==0)tarjan(i);
}
for (register int i=1;i<=n;i++)
{
cout<<group[i]<<endl;
}
return 0;
}
(然而luogu的模板还是有点毒瘤的)
看一个经典例题。
例题 Luogu P2341 [HAOI2006]受欢迎的牛 |【模板】强连通分量
题目描述
每头奶牛都梦想成为牛棚里的明星。被所有奶牛喜欢的奶牛就是一头明星奶牛。所有奶
牛都是自恋狂,每头奶牛总是喜欢自己的。奶牛之间的“喜欢”是可以传递的——如果A喜
欢B,B喜欢C,那么A也喜欢C。牛栏里共有N 头奶牛,给定一些奶牛之间的爱慕关系,请你
算出有多少头奶牛可以当明星。
输入格式
第一行:
两个用空格分开的整数:N和M第二行到第M + 1行:
每行两个用空格分开的整数:A和B,表示A喜欢B输出格式
第一行:单独一个整数,表示明星奶牛的数量
数据范围
\(10\%\)的数据\(N\leq 20, M\leq 50\)
\(30\%\)的数据\(N\leq 1000,M\leq 20000\)
\(70\%\)的数据\(N\leq 5000,M\leq 50000\)
\(100\%\)的数据\(N\leq 10000,M\leq 50000\)
可了不得这题怎么看不懂啊
现在看到这个“A受B欢迎”应当是指A向B连接一条有向边。
然后...一个强连通分量里面只要大小不为1,这里面的所有奶牛都互相喜欢。
那么我们只要缩一遍点,那么找到出度为0的强连通分量,那么所有的奶牛都会喜欢这个强连通分量里的奶牛。
所以正确处理方式就是:
- 缩点
- 找到出度为0的点。
但是注意如果要是两个出度为0的强连通分量,那可不就是没有明星了。
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 1e5 + 5, maxm = 5e5 + 5;
struct edge
{
int to, next;
};
edge Edge[maxm];
int cnt = 1, head[maxn];
inline void add_edge(int from, int to)
{
Edge[cnt].to = to;
Edge[cnt].next = head[from];
head[from] = cnt++;
}
struct STACK
{
int s[maxn];
int up;
inline int top()
{
return s[up];
}
inline void pop()
{
up ? up-- : up;
}
inline void push(int v)
{
s[++up] = v;
}
STACK() { up = 0; }
};
STACK s;
int dfn[maxn], group[maxn], low[maxn], dgr[maxn], sz[maxn];
int n, m, dfncnt, gcnt;
bool vst[maxn];
void tarjan(int n)
{
dfn[n] = low[n] = ++dfncnt;
s.push(n);
vst[n] = true;
for (register int i = head[n]; i; i = Edge[i].next)
{
int to = Edge[i].to;
if (!dfn[to])
{
tarjan(to);
low[n] = min(low[n], low[to]);
}
else if (vst[to])
low[n] = min(low[n], dfn[to]);
}
if (dfn[n] == low[n])
{
group[n] = ++gcnt;
int now;
do
{
now = s.top();
s.pop();
vst[now] = false;
group[now] = gcnt;
sz[gcnt]++;
} while(now != n);
}
}
int main()
{
scanf("%d%d", &n, &m);
for (register int i = 1; i <= m; i++)
{
int x, y;
scanf("%d%d", &x, &y);
add_edge(x, y);
}
for (register int i = 1; i <= n; i++)
if (!dfn[i])
tarjan(i);
for (register int i = 1; i <= n; i++)
{
for (register int e = head[i]; e; e = Edge[e].next)
{
int to = Edge[e].to;
if (group[i] != group[to])
dgr[group[i]]++;
}
}
int one = 0;
for (int i = 1; i <= gcnt; i++)
{
if (dgr[i] == 0)
{
if (one)
{
puts("0");
return 0;
}
one = i;
}
}
printf("%d\n", sz[one]);
return 0;
}
一类有向图dp
对于一般在有向图(不保证是DAG)上面跑DP的时候,如果不能处理后效性,
那么通常我们可以通过先缩点再拓扑排序跑DP。
例1 LuoguP3387【模板】缩点
题目背景
缩点+DP
题目描述
给定一个n个点m条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。
允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。
输入格式
第一行,n,m
第二行,n个整数,依次代表点权
第三至m+2行,每行两个整数u,v,表示u->v有一条有向边
输出格式
共一行,最大的点权之和。
这个题我们可以先跑一边tarjan缩点,然后再来一发toposort,做“我为人人”类型的dp。
代码实现:
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
template <typename Tp>
inline Tp Read()
{
Tp num = 0;
char ch = getchar();
bool flag = false;
while (!isdigit(ch))
flag |= ch == '-', ch = getchar();
while (isdigit(ch))
num = (num << 1) + (num << 3) + (ch ^ 48), ch = getchar();
return flag ? -num : num;
}
struct gragh
{
struct edge
{
int from, to, next;
};
edge Edge[100086];
int cnt, head[10086];
inline void add_edge(int from, int to)
{
this->cnt++;
this->Edge[this->cnt] = (edge){from, to, this->head[from]};
this->head[from] = this->cnt;
}
} a, b;
int n, m, val[10086];
// tarjan uses
int dfn[10086], low[10086], stack[10086], dfncnt, top;
bool instack[10086];
int group[10086], gcnt, gval[10086], In[10086];
// 缩点 解决dp后效性问题
void tarjan(int n)
{
dfn[n] = low[n] = ++dfncnt;
stack[++top] = n;
instack[n] = true;
for (int i = a.head[n]; i; i = a.Edge[i].next)
{
int to = a.Edge[i].to;
if (dfn[to] == 0)
{
tarjan(to);
low[n] = min(low[n], low[to]);
}
else
{
if (instack[to])
{
low[n] = min(low[n], low[to]);
}
}
}
if (dfn[n] == low[n])
{
int now;
gcnt++;
// gval[gcnt]=val[n];
do
{
now = stack[top--];
instack[now] = false;
group[now] = gcnt;
gval[gcnt] += val[now];
} while (now != n);
}
}
// rebuild the gragh
void rebuild()
{
for (int i = 1; i <= a.cnt; i++)
{
int from = a.Edge[i].from, to = a.Edge[i].to;
if (group[from] != group[to])
{
b.add_edge(group[from], group[to]);
In[group[to]]++;
}
}
}
int dis[10086], ans = 0;
// dp on the DAG
void toposort()
{
queue<int> q;
for (int i = 1; i <= gcnt; i++)
{
if (In[i] == 0)
q.push(i);
dis[i] = gval[i];
}
while (!q.empty())
{
int from = q.front();
q.pop();
for (int i = b.head[from]; i; i = b.Edge[i].next)
{
int to = b.Edge[i].to;
dis[to] = max(dis[from] + gval[to], dis[to]);
In[to]--;
if (In[to] == 0)
q.push(to);
}
}
for (int i = 1; i <= gcnt; i++)
{
ans = max(ans, dis[i]);
}
}
int main()
{
n = Read<int>(), m = Read<int>();
for (int i = 1; i <= n; i++)
val[i] = Read<int>();
for (int i = 1; i <= m; i++)
{
a.add_edge(Read<int>(), Read<int>());
}
for (int i = 1; i <= n; i++)
if (dfn[i] == 0)
{
tarjan(i);
}
rebuild();
toposort();
printf("%d\n", ans);
return 0;
}
例2 LuoguP3916 图的遍历
题目描述
给出\(N\)个点,\(M\)条边的有向图,对于每个点\(v\),求\(A(v)\),\(A(v)\)表示从点\(v\)出发,能到达的编号最大的点。
输入格式
第1 行,2 个整数\(N,M\)。
接下来\(M\)行,每行2个整数\(U_i,V_i\),表示边\((U_i,V_i)\)。点用\(1, 2,\cdots,N\)编号。
输出格式
\(n\)个整数\(A(1),A(2),\cdots,A(N)\)。
数据范围
对于\(60\%\)的数据,\(1\leq N,K \leq 10^3\)
对于\(100\%\)的数据,\(1\leq N, M \leq 10^5\)
明显的就是一个dfs或者bfs就可以搞掉的题。
然而我们还可以通过先缩点后topo sort套dp的方式优雅AC。
代码实现:
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
struct edge
{
int from, to, next;
};
struct gragh
{
edge Edge[100086];
int cnt, head[100086];
inline void add_edge(int from, int to)
{
this->cnt++;
this->Edge[this->cnt].from = from;
this->Edge[this->cnt].to = to;
this->Edge[this->cnt].next = this->head[from];
this->head[from] = cnt;
}
};
gragh a, b;
int dfn[100086], low[100086], dfncnt, stack[100086], top;
int group[100086], gcnt, maxid[100086];
bool vst[100086];
int n, m, In[100086], dp[100086];
void tarjan(int n)
{
dfn[n] = low[n] = ++dfncnt;
stack[++top] = n;
vst[n] = true;
for (int i = a.head[n]; i; i = a.Edge[i].next)
{
int to = a.Edge[i].to;
if (dfn[to] == 0)
{
tarjan(to);
low[n] = min(low[n], low[to]);
}
else if (vst[to])
{
low[n] = min(low[n], dfn[to]);
}
}
if (low[n] == dfn[n])
{
int now;
gcnt++;
do
{
now = stack[top--];
group[now] = gcnt;
maxid[gcnt] = max(maxid[gcnt], now);
vst[now] = false;
} while (now != n);
}
}
void rebuild()
{
for (int i = 1; i <= a.cnt; i++)
{
int from = a.Edge[i].from;
int to = a.Edge[i].to;
if (group[from] != group[to])
{
In[group[to]]++;
b.add_edge(group[from], group[to]);
}
}
}
void toposort()
{
queue<int> q;
for (int i = 1; i <= gcnt; i++)
if (In[i] == 0)
q.push(i);
while (!q.empty())
{
int from = q.front();
q.pop();
dp[from] = max(dp[from], maxid[from]);
for (int i = b.head[from]; i; i = b.Edge[i].next)
{
int to = b.Edge[i].to;
dp[to] = max(dp[from], dp[to]);
if (--In[to] == 0)
q.push(to);
}
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
int f, t;
scanf("%d%d", &f, &t);
a.add_edge(t, f);
}
for (int i = 1; i <= n; i++)
{
if (dfn[i] == 0)
tarjan(i);
}
rebuild();
toposort();
for (int i = 1; i <= n; i++)
{
printf("%d ", dp[group[i]]);
}
return 0;
}
剩下的先咕着
tarjan复习笔记的更多相关文章
- tarjan复习笔记 双连通分量,强连通分量
声明:图自行参考割点和桥QVQ 双连通分量 如果一个无向连通图\(G=(V,E)\)中不存在割点(相对于这个图),则称它为点双连通图 如果一个无向连通图\(G=(V,E)\)中不存在割边(相对于这个图 ...
- tarjan 复习笔记 割点与桥
定义分析 给定一个无向连通图\(G=(V,E)\) 对于\(x\in Y\),如果删去\(x\)及与\(x\)相连的边后,\(G\)分裂为两个或者两个以上的不连通子图,那么称\(x\)为\(G\)的割 ...
- Java基础复习笔记系列 九 网络编程
Java基础复习笔记系列之 网络编程 学习资料参考: 1.http://www.icoolxue.com/ 2. 1.网络编程的基础概念. TCP/IP协议:Socket编程:IP地址. 中国和美国之 ...
- Java基础复习笔记系列 八 多线程编程
Java基础复习笔记系列之 多线程编程 参考地址: http://blog.csdn.net/xuweilinjijis/article/details/8878649 今天的故事,让我们从上面这个图 ...
- Java基础复习笔记系列 七 IO操作
Java基础复习笔记系列之 IO操作 我们说的出入,都是站在程序的角度来说的.FileInputStream是读入数据.?????? 1.流是什么东西? 这章的理解的关键是:形象思维.一个管道插入了一 ...
- Java基础复习笔记系列 五 常用类
Java基础复习笔记系列之 常用类 1.String类介绍. 首先看类所属的包:java.lang.String类. 再看它的构造方法: 2. String s1 = “hello”: String ...
- Java基础复习笔记系列 四 数组
Java基础复习笔记系列之 数组 1.数组初步介绍? Java中的数组是引用类型,不可以直接分配在栈上.不同于C(在Java中,除了基础数据类型外,所有的类型都是引用类型.) Java中的数组在申明时 ...
- Java基础复习笔记基本排序算法
Java基础复习笔记基本排序算法 1. 排序 排序是一个历来都是很多算法家热衷的领域,到现在还有很多数学家兼计算机专家还在研究.而排序是计算机程序开发中常用的一种操作.为何需要排序呢.我们在所有的系统 ...
- Angular复习笔记7-路由(下)
Angular复习笔记7-路由(下) 这是angular路由的第二篇,也是最后一篇.继续上一章的内容 路由跳转 Web应用中的页面跳转,指的是应用响应某个事件,从一个页面跳转到另一个页面的行为.对于使 ...
随机推荐
- 网络流强化-HDU 3338-上下界限制最大流
题意是: 一种特殊的数独游戏,白色的方格给我们填1-9的数,有些带数字的黑色方格,右上角的数字代表从他开始往右一直到边界或者另外一个黑格子,中间经过的白格子的数字之和要等于这个数字:左下角的也是一样的 ...
- pandas dataframe 一行变多行 (query pv统计term pv)
关键字: 用jieba切词 用expand 一列变多列 用stack 列转行 用group by + aggr 相同term的pv求和 上效果: query pv 今日新鲜事 今日头条 北京天气 上海 ...
- linux下的sleep()和usleep()的使用和区别
函数名: sleep头文件: #include<windows.h> // 在VC中使用带上头文件 #include<unistd.h> // ...
- 公司C++规范学习
目录 公司C++规范学习 语法部分 风格/约定 公司C++规范学习 语法部分 class和struct关键字的选择:class表示被封装的用户自定义类型,不公开定义非静态数据成员,struct表示数据 ...
- 求bit中1的个数有几种做法
原文 求bit中1的个数有几种做法: - x & (x - 1) - Hamming weight的经典求法,基于树状累加:http://en.wikipedia.org/wiki/Hammi ...
- [Linux] 027 RPM 包与 源码包的区别
1. 区别 安装之前的区别: 概念上的区别 安装之后的区别: 安装位置不同 2. RPM 包安装位置 安装在默认位置中 RPM 包默认安装路径 明细 /ect 配置文件安装目录 /usr/bin/ 可 ...
- Synchronized 详解
为了方便记忆,将锁做如下的分类 一.对象锁 包括方法锁(默认锁对象为this,当前实例对象)和同步代码块锁(自己指定锁对象) 1.代码块形式:手动指定锁定对象,也可是是this,也可以是自定义的锁 p ...
- CentOS删除Applications中的菜单项
有时候会错误的安装一些软件,可能安装被不成功,但是在左上角的Applications菜单中还是会显示出来,让人很不爽. 现在介绍一个删除掉CentOS Applications中菜单项的方法: 1.安 ...
- mpvue中的平台状态判断(H5网页 or 小程序)
在开发微信小程序或者微信网页H5的时候,有时我们利用外部组件可能不兼容这两者,需要区分开来,可以在对应的mainjs中配置如下 let platform: try{ if(wx){ platform= ...
- 解决嵌套在ScrollView中的TableView滑动手势冲突问题
最近在迭代开发公司项目的时候遇到了一个问题,在可以左右切换标签视图的ScrollView中嵌套了两个TableView用于展示视图,感觉一切so easy的情况下,问题出现了,因为左右两个视图既可以实 ...