Tarjan 求强连通分量 O ( n + m ) O(n+m) O(n+m) ,缩点
极其后悔当初没写博客
强连通:若一张 有向图 的节点两两互相可达,则称这张图是强连通的。
强连通分量(SCC): 极大的强连通子图。
对图 dfs 时,每个节点只访问一次,被访问过的节点和边构成搜索树。
有向边分为四种情况:
- 树边: 访问节点走过的边。黑色。
- 返租边:指向祖先节点的边。红色。
- 横叉边:柚子树指向左子树的边。绿色。
- 前向边:指向子树中节点的边。蓝色。
返租边和树边一定构成环,横叉边可能与树边构成环,前向边无用
如果节点 X 是某个强连通分量在 搜索树 中遇到的第一个节点,则称 X 是这个强连通分量的根。
- 时间戳 d f n [ ] dfn [\ ] dfn[ ] :节点 X 第一次被访问的顺序。
- 追溯值 l o w [ ] low[\ ] low[ ]: 从节点 X 出发,所能访问到的最早的时间戳。
int dfn[N],low[N],cnt;
vector<int>scc[N];
int stk[N],top,tot,siz[N],id[N];//栈,栈顶,某个SCC节点数量
bool instk[N];
// 入、回、离、三部分。
void tarjan(int x){
// x 【入】栈,盖戳
dfn[x] = low[x] = ++cnt;
stk[++top] = x;
instk[x] = 1;
// 遍历 x 邻接点,【回】X
for(int i=h[x];~i;i=ne[i]){
int j = e[i];
if(!dfn[j]){
tarjan(j);
low[x] = min(low[x],low[j]); // 如果j有返祖边,需要更新low[x]
}
else if(instk[j]){ // j已经被访问并且已经在栈中,横叉边和返祖边
low[x] = min(low[x],dfn[j]);
}
}
// 【离】开X,记录SCC
if(dfn[x] == low[x]){ // x 是某个SCC的根节点
int y; tot++;
do{
y = stk[top--];instk[y]=0;
scc[tot].pb(y);
id[y]=tot;
siz[tot]++;
}while(y!=x);
}
}
从未思考过的问题
- 为什么要有 l o w [ ] low[\ ] low[ ] ?
low[x] = min(low[x],low[y]) 如果去掉的话,遍历到最低端的节点,想通过祖先边返回的时候,会提前当成一个SCC出栈。
- l o w [ x ] = m i n ( l o w [ x ] , d f n [ y ] ) low[x] = min(low[x],dfn[y]) low[x]=min(low[x],dfn[y]) 可以换成 l o w [ y ] low[y] low[y] 吗?
求 S C C SCC SCC 可以换,但是求 无向边的双连通分量就会出错。所以 不要换!
P3387 【模板】缩点
终于把这个坑给填上了
建图 -> tarjan 求SCC -> 新的SCC之间连边 -> topsort -> 遍历得最值。
一开始我还想原图上的点和SCC连边,贼乱,还只有60分。
还有就是直接在topsort中dp。
for(int i=1;i<=n;i++)
{
for(int j=h[i];~j;j=ne[j])
{
int y=e[j];
if(id[i]==id[y])continue;
adds(id[i],id[y]);
// 直接在新的连通分量之间连边。
d[id[y]]++;
}
}
// Problem: P3387 【模板】缩点
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3387
// Memory Limit: 125 MB
// Time Limit: 1000 ms
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <queue>
#include <set>
#include <map>
#include <vector>
#include <sstream>
#define pb push_back
#define in insert
#define mem(f, x) memset(f,x,sizeof(f))
#define fo(i,a,n) for(int i=(a);i<=(n);++i)
#define fo_(i,a,n) for(int i=(a);i<(n);++i)
#define debug(x) cout<<#x<<":"<<x<<endl;
#define endl '\n'
using namespace std;
template<typename T>
ostream& operator<<(ostream& os,const vector<T>&v){for(int i=0,j=0;i<v.size();i++,j++)if(j>=5){j=0;puts("");}else os<<v[i]<<" ";return os;}
template<typename T>
ostream& operator<<(ostream& os,const set<T>&v){for(auto c:v)os<<c<<" ";return os;}
typedef pair<int,int>PII;
typedef pair<long,long>PLL;
typedef long long ll;
typedef unsigned long long ull;
const int N=2e5+10,M=1e5+10;
int n,m;
int w[N];//缩点之前的权值
int h[N],e[N],ne[N],idx;
int hs[N],es[N],nes[N],idxs;
int dfn[N],low[N],tim;
int stack[N],top;
bool is_in[N];
int cnt;//存储SCC个数
int id[N]; // 存储每个节点在哪个SCC中
vector<int>scc[N];
int W[N];//缩点之后的权值
void add(int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void adds(int a,int b){
es[idxs]=b,nes[idxs]=hs[a],hs[a]=idxs++;
}
void tarjan(int u){
dfn[u]=low[u]=++tim;
stack[++top]=u;
is_in[u]=1;
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(!dfn[j]){
tarjan(j);
low[u]=min(low[u],low[j]);
}
else if(is_in[j]){
low[u]=min(low[u],dfn[j]);
}
}
if(dfn[u]==low[u]){
int y;
cnt++;
do{
y=stack[top--];
is_in[y]=false;
scc[cnt].pb(y);
W[cnt]+=w[y];
id[y]=cnt;
}while(u!=y);
}
}
int d[N],Topo[N];
int Cnt;//拓扑序下标
ll dis[N];
void topo()
{
queue<int>que;
for(int i=1;i<=cnt;i++)
if(!d[i]){
que.push(i);
Topo[++Cnt]=i;
dis[i] = W[i];
}
while(que.size())
{
int t=que.front();
que.pop();
for(int i=hs[t];~i;i=nes[i])
{
int j=es[i];
d[j]--;
if(!d[j])
{
que.push(j);
dis[j] = max(dis[j] , dis[t] + W[j]);
Topo[++Cnt]=j;
}
}
}
}
void solve()
{
mem(h,-1);
mem(hs,-1);
cin>>n>>m;
fo(i,1,n)cin>>w[i];
fo(i,1,m){
int a,b;cin>>a>>b;
add(a,b);
}
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i);
for(int i=1;i<=n;i++)
{
for(int j=h[i];~j;j=ne[j])
{
int y=e[j];
if(id[i]==id[y])continue;
adds(id[i],id[y]);
// 直接在新的连通分量之间连边。
d[id[y]]++;
}
}
topo();
ll ans = 0;
fo(i,1,Cnt){
ans = max(ans,dis[i]);
}
cout<<ans<<endl;
}
int main()
{
solve();
return 0;
}
P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G
题目描述
每头奶牛都梦想成为牛棚里的明星。被所有奶牛喜欢的奶牛就是一头明星奶牛。所有奶牛都是自恋狂,每头奶牛总是喜欢自己的。奶牛之间的“喜欢”是可以传递的——如果 A A A 喜欢 B B B, B B B 喜欢 C C C,那么 A A A 也喜欢 C C C。牛栏里共有 N N N 头奶牛,给定一些奶牛之间的爱慕关系,请你算出有多少头奶牛可以当明星。
输入格式
第一行:两个用空格分开的整数: N N N 和 M M M。
接下来 M M M 行:每行两个用空格分开的整数: A A A 和 B B B,表示 A A A 喜欢 B B B。
对于 100 % 100\% 100% 的数据, 1 ≤ N ≤ 1 0 4 1\le N\le10^4 1≤N≤104, 1 ≤ M ≤ 5 × 1 0 4 1\le M\le5\times 10^4 1≤M≤5×104。
输出格式
一行单独一个整数,表示明星奶牛的数量。
容易发现,只有一个SCC可能是答案。然后我想着缩点(不用建图)统计SCC的入度,当入度 == cnt-1时统计答案,如果有多个满足条件ans = 0。
然后被hack了
因为可能SCC有间接入度。所以应该统计出度。AC!
// Problem: P3387 【模板】缩点
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3387
// Memory Limit: 125 MB
// Time Limit: 1000 ms
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <queue>
#include <set>
#include <map>
#include <vector>
#include <sstream>
#define pb push_back
#define in insert
#define mem(f, x) memset(f,x,sizeof(f))
#define fo(i,a,n) for(int i=(a);i<=(n);++i)
#define fo_(i,a,n) for(int i=(a);i<(n);++i)
#define debug(x) cout<<#x<<":"<<x<<endl;
#define endl '\n'
using namespace std;
template<typename T>
ostream& operator<<(ostream& os,const vector<T>&v){for(int i=0,j=0;i<v.size();i++,j++)if(j>=5){j=0;puts("");}else os<<v[i]<<" ";return os;}
template<typename T>
ostream& operator<<(ostream& os,const set<T>&v){for(auto c:v)os<<c<<" ";return os;}
typedef pair<int,int>PII;
typedef pair<long,long>PLL;
typedef long long ll;
typedef unsigned long long ull;
const int N=1e4+10,M=1e5+10;
int n,m;
int h[N],e[10*N],ne[10*N],idx;
vector<int>scc[N];
int sz[N],cnt;//存储SCC个数
int dfn[N],low[N],tim;
int stack[N],top;
int id[N],d[N];
bool is_in[N];
void tarjan(int u){
dfn[u] = low[u] = ++tim;
stack[top++] = u;
is_in[u] = true;
for(int i=h[u];i!=-1;i=ne[i]){
int v = e[i];
if(!dfn[v]){
tarjan(v);
low[u] = min(low[u],low[v]);
}
else if(is_in[v])
low[u] = min(low[u],dfn[v]);
}
if(dfn[u] == low[u]){
int v;
cnt++;
do{
v = stack[--top];
is_in[v] = false;
scc[cnt].pb(v);
sz[cnt]++;
id[v] = cnt;
}while(v != u);
}
}
void add(int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void solve(){
mem(h,-1);
cin>>n>>m;
fo(i,1,m){
int a,b;cin>>a>>b;
add(a,b);
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
tarjan(i);
}
}
for(int i=1;i<=n;i++){
for(int k=h[i];~k;k=ne[k]){
int j=e[k];
if(id[i] == id[j])continue;
d[id[i]]++;
}
}
ll ans = 0;
int num = 0;
fo(i,1,cnt){
if(!d[i]){
num++;
ans += sz[i];
}
}
if(num>1)ans = 0;
cout<<ans;
}
int main()
{
solve();
return 0;
}
采蘑菇 (缩点应用topo最长路(带边权和点权) + 卡double精度!)
题目描述
小胖和 ZYR 要去 ESQMS 森林采蘑菇。
ESQMS 森林间有 N N N 个小树丛, M M M 条小径,每条小径都是单向的,连接两个小树丛,上面都有一定数量的蘑菇。小胖和 ZYR 经过某条小径一次,可以采走这条路上所有的蘑菇。由于 ESQMS 森林是一片神奇的沃土,所以一条路上的蘑菇被采过后,又会长出一些新的蘑菇,数量为原来蘑菇的数量乘上这条路的“恢复系数”,再下取整。
比如,一条路上有 4 4 4 个蘑菇,这条路的“恢复系数”为 0.7 0.7 0.7,则第一~四次经过这条路径所能采到的蘑菇数量分别为 4 , 2 , 1 , 0 4,2,1,0 4,2,1,0。
现在,小胖和 ZYR 从 S S S 号小树丛出发,求他们最多能采到多少蘑菇。
输入格式
第一行两个整数, N N N 和 M M M。
第二行到第 M + 1 M+1 M+1 行,每行四个数,分别表示一条小路的起点,终点,初始蘑菇数,恢复系数。
第 M + 2 M+2 M+2 行,一个整数 S S S。
输出格式
一行一个整数,表示最多能采到多少蘑菇,保证答案不超过 ( 2 31 − 1 ) (2^{31}-1) (231−1)。
样例输入
3 3
1 2 4 0.5
1 3 7 0.1
2 3 4 0.6
1
样例输出
8
对于 100 % 100\% 100% 的数据, 1 ≤ N ≤ 8 × 1 0 4 1 \le N\le 8\times 10^4 1≤N≤8×104, 1 ≤ M ≤ 2 × 1 0 5 1\le M\le 2\times 10^5 1≤M≤2×105, 0 ≤ 恢复系数 ≤ 0.8 0\le\text{恢复系数}\le 0.8 0≤恢复系数≤0.8 且最多有一位小数, 1 ≤ S ≤ N 1\le S\le N 1≤S≤N。
和缩点求最长路板子唯一的不同就是点权变成了边权。缩点,对每个SCC暴力求他的内部边权和记作他的点权。然后跑Topo求最长路。
终于调出了样例,信心满满,一发上去10分qwq
后来终于30分了,想查看样例1发现竟然乱码了?!?在谷友和管理帮助下得到了样例1的数据,写字板竟然可以看到正确数据。发现是我先 * 10 再读入的,麻了。
这题卡精度,需要先乘10再/10,全程除了读入不用double!
还有就是怎么用拓扑排序dp。
拓扑求最长路
初始点不一样是0读入点,把他先塞进去,其他的零入读点也赛进入,然后初始成 -0x3f3f3f3f 。
topo 的时候,更新一定要将所有临界点更新,不能放里边更新!
void topo()
{
queue<int>que;
for(int i=1;i<=cnt;i++){
if(!d[i]){
que.push(i);
}
dis[i] = -0x3f3f3f3f3f3f3f3f;
}
root = id[root];
dis[root] = W[root];
while(que.size()){
int t=que.front();
que.pop();
for(int i=hs[t];~i;i=nes[i])
{
int j=es[i];
int edge_value = Ws[i];
// ? 放if里边和外边竟然不同 , 一定要放外卖,里边会wa!
dis[j] = max(dis[j] , dis[t] + W[j] + edge_value);
d[j]--;
if(!d[j]){
que.push(j);
}
}
}
}
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <queue>
#include <set>
#include <map>
#include <vector>
#include <sstream>
#define pb push_back
#define in insert
#define mem(f, x) memset(f,x,sizeof(f))
#define fo(i,a,n) for(int i=(a);i<=(n);++i)
#define fo_(i,a,n) for(int i=(a);i<(n);++i)
#define debug(x) cout<<#x<<":"<<x<<endl;
#define endl '\n'
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int N=2e5+10,M=1e5+10;
int n,m;
int h[N],e[N],ne[N],w[N],idx,p[N];
int hs[N],es[N],nes[N],idxs;
int Ws[N];
int dfn[N],low[N],tim;
int stack[N],top;
bool is_in[N];
int cnt;//存储SCC个数
int id[N]; // 存储每个节点在哪个SCC中
vector<int>scc[N];
int W[N];//缩点之后的权值
int root;
void add(int a,int b,int c,int d){
e[idx]=b,ne[idx]=h[a],w[idx]=c,p[idx]=d,h[a]=idx++;
}
void adds(int a,int b,int c){
es[idxs]=b,nes[idxs]=hs[a];
Ws[idxs]=c;
hs[a]=idxs++;
}
void tarjan(int u){
dfn[u]=low[u]=++tim;
stack[++top]=u;
is_in[u]=1;
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(!dfn[j]){
tarjan(j);
low[u]=min(low[u],low[j]);
}
else if(is_in[j]){
low[u]=min(low[u],dfn[j]);
}
}
if(dfn[u]==low[u]){
int y;
cnt++;
do{
y=stack[top--];
is_in[y]=false;
scc[cnt].pb(y);
id[y]=cnt;
}while(u!=y);
}
}
int d[N];
ll dis[N];
void topo()
{
queue<int>que;
for(int i=1;i<=cnt;i++){
if(!d[i]){
que.push(i);
}
dis[i] = -0x3f3f3f3f3f3f3f3f;
}
root = id[root];
dis[root] = W[root];
while(que.size()){
int t=que.front();
que.pop();
for(int i=hs[t];~i;i=nes[i])
{
int j=es[i];
int edge_value = Ws[i];
// ? 放if里边和外边竟然不同 , 一定要放外卖,里边会wa!
dis[j] = max(dis[j] , dis[t] + W[j] + edge_value);
d[j]--;
if(!d[j]){
que.push(j);
}
}
}
}
void solve()
{
mem(h,-1);
mem(hs,-1);
cin>>n>>m;
fo(i,1,m){
int a,b,c;
double d;
cin>>a>>b>>c>>d;
d*=10;
add(a,b,c,d);
}
cin>>root;
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i);
for(int i=1;i<=n;i++)
{
for(int j=h[i];~j;j=ne[j])
{
int y=e[j];
int value=w[j];
int cost=p[j];
ll sum = 0;
if(id[i]==id[y]){
// i,y value
while(value){
sum += value;
value = value*cost/10;
}
W[id[i]] += sum;
continue;
}
adds(id[i],id[y],value); // 只能走一遍了
d[id[y]]++;
// 直接在新的连通分量之间连边。
}
}
topo();
ll ans = 0;
fo(i,1,cnt){
ans = max(ans,dis[i]);
}
cout<<ans<<endl;
}
int main()
{
solve();
return 0;
}