Tarjan

强连通分量

前置知识

  • 强连通 :一张有向图的节点两两互相可达。
  • 连通分量 : 若HG的一个连通子图,且不存在F使得HFG,则HG的一个连通分量(也叫连通块)

Tarjan求强连通分量

DFS生成树

image

用的OI-wiki的图

  1. 树边 : 图中的黑边,每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。
  2. 返祖边/回边 : 图中的红色边,指向父亲节点。
  3. 横叉边 : 图中的蓝边,搜索时访问到一个已经访问过的节点,但这个节点不是当前节点的祖先。
  4. 前向边 : 图中的绿边,搜索时访问到子树的节点

那么这个有什么用呢?

如果结点u是某个强连通分量在搜索树中遇到的第一个结点,那么这个强连通分量的其余结点肯定是在搜索树中以u为根的子树中。结点u被称为这个强连通分量的根。

证明

假如有一个节点v在该强连通分量中但是不在u的子树中,那么v肯定尚未访问,那么uv的路径中一定有一条边离开了u的子树,这样的边只能是返祖边或横叉边,然而这两条边都指向了访问过的节点,矛盾。

考虑用这个结论,得出Tarjan求强连通分量的算法

Tarjan求强连通分量

基于对图进行深度优先搜索。

先定义两个数组,对于每个节点u,有

  1. dfnu (时间戳): dfs时节点u被遍历的次序
  2. lowu (追溯值): 记以u为根的子树为Subtreeulowu定义为Subtreeu中的节点经过一条非树边可以达到的节点的dfn的最小值。

有两个非常显然的结论,一个节点子树内的dfn都大于该点的dfn,从根开始的一条路径上,dfn单调递增,low不严格递减。提一嘴,万一以后有用呢?虽然到现在为止没用过

算法流程 :

深度优先搜索,维护每个节点的dfnlow,将搜索到的节点入栈。假设当前搜索到的节点为u,与u连边的一个节点为vv不是u的父亲),此时有三种情况

  1. v未被访问过,那么继续搜索,用lowv更新lowu
  2. v被访问过且已经在栈中,那么用dfnv更新lowu
  3. v被访问过且不在栈中,那么表示v已经解决掉了,不需要再对其进行操作。

根据强连通分量的根为强连通分量中第一个被搜索到的节点,所以其dfnlow最小。容易得出当且仅当强连通分量的根,其dfn=low,并且强连通分量中的节点就是栈中u上方的节点。

时间复杂度O(n+m)

模板:强连通分量

点此查看代码
#include<bits/stdc++.h>
#include<bits/extc++.h>
// using namespace __gnu_pbds;
// using namespace __gnu_cxx;
using namespace std;
#define infile(x) freopen(x,"r",stdin)
#define outfile(x) freopen(x,"w",stdout)
#define errfile(x) freopen(x,"w",stderr)
#define rep(i,s,t,p) for(int i = s;i <= t; i += p)
#define drep(i,s,t,p) for(int i = s;i >= t; i -= p)
#ifdef LOCAL
FILE *InFile = infile("in.in"),*OutFile = outfile("out.out");
// FILE *ErrFile=errfile("err.err");
#else
FILE *Infile = stdin,*OutFile = stdout;
//FILE *ErrFile = stderr;
#endif
using ll=long long;using ull=unsigned long long;
using db = double;using ldb = long double;
const int N = 1e4 + 10;
vector<int> edge[N];
#define eb emplace_back
inline void add(int u,int v){edge[u].eb(v);}
int n,m,dfn[N],low[N],sta[N],top,tot,num,bel[N];
bitset<N> insta,out;
vector<int> scc[N];
void Tarjan(int x){
dfn[x] = low[x] = ++tot;
sta[++top] = x;insta[x] = true;
for(int y:edge[x]){
if(!dfn[y]){
Tarjan(y);
low[x] = min(low[x],low[y]);
}
else if(insta[y]) low[x] = min(low[x],dfn[y]);
}
if(dfn[x] == low[x]){
num++;int y;
do{
y = sta[top--];
bel[y] = num;
scc[num].eb(y),insta[y] = false;
}while(y != x);
}
}
inline void solve(){
cin>>n>>m;
rep(i,1,m,1){
int u,v;cin>>u>>v;
add(u,v);
}
rep(i,1,n,1) if(!dfn[i]) Tarjan(i);
rep(i,1,num,1) sort(scc[i].begin(),scc[i].end());
cout<<num<<'\n';
rep(i,1,n,1){
if(out[bel[i]]) continue;
for(int j:scc[bel[i]]) cout<<j<<' ';
cout<<'\n';
out[bel[i]] = true;
}
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(false);
cout.tie(nullptr)->sync_with_stdio(false);
solve();
}

例题 :

  1. 受欢迎的牛

就是求强连通分量,然后看每个强连通分量的出度,如果出度为0的强连通分量有一个以上,那么无解,反之答案就是出度为0的那个强连通分量的大小

点此查看代码
#include<bits/stdc++.h>
#include<bits/extc++.h>
// using namespace __gnu_pbds;
// using namespace __gnu_cxx;
using namespace std;
#define infile(x) freopen(x,"r",stdin)
#define outfile(x) freopen(x,"w",stdout)
#define errfile(x) freopen(x,"w",stderr)
#define rep(i,s,t,p) for(int i = s;i <= t; i += p)
#define drep(i,s,t,p) for(int i = s;i >= t; i -= p)
#ifdef LOCAL
FILE *InFile = infile("in.in"),*OutFile = outfile("out.out");
// FILE *ErrFile=errfile("err.err");
#else
FILE *Infile = stdin,*OutFile = stdout;
//FILE *ErrFile = stderr;
#endif
using ll=long long;using ull=unsigned long long;
using db = double;using ldb = long double;
const int N = 1e4 + 10;
vector<int> edge[N];
#define eb emplace_back
inline void add(int u,int v){edge[u].eb(v);}
int n,m,dfn[N],low[N],sta[N],top,tot,num,bel[N],out[N],len[N];
bitset<N> insta;
void Tarjan(int x){
dfn[x] = low[x] = ++tot;
sta[++top] = x;insta[x] = true;
for(int y:edge[x]){
if(!dfn[y]){
Tarjan(y);
low[x] = min(low[x],low[y]);
}
else if(insta[y]) low[x] = min(low[x],dfn[y]);
}
if(dfn[x] == low[x]){
num++;int y;
do{
y = sta[top--];
bel[y] = num;
insta[y] = false;
len[num]++;
}while(y != x);
}
}
inline void solve(){
cin>>n>>m;
rep(i,1,m,1){
int u,v;cin>>u>>v;
add(u,v);
}
rep(i,1,n,1) if(!dfn[i]) Tarjan(i);
int ans = 0,ok = 0;
rep(x,1,n,1) for(int y:edge[x]) if(bel[x] != bel[y]) out[bel[x]]++;
rep(i,1,num,1) if(!out[i])ans = len[i],ok++;
if(ok > 1) cout<<0;
else cout<<ans;
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(false);
cout.tie(nullptr)->sync_with_stdio(false);
solve();
}
  1. 校园网络【[USACO]Network of Schools加强版】

先缩点,第一问的答案就是缩完点后入度为0的点的个数,第二问的答案就是缩完点后max(00)。特判只有一个强连通分量时,第一问为1,第二问为0。

点此查看代码
#include<bits/stdc++.h>
#include<bits/extc++.h>
// using namespace __gnu_pbds;
// using namespace __gnu_cxx;
using namespace std;
#define infile(x) freopen(x,"r",stdin)
#define outfile(x) freopen(x,"w",stdout)
#define errfile(x) freopen(x,"w",stderr)
#define rep(i,s,t,p) for(int i = s;i <= t; i += p)
#define drep(i,s,t,p) for(int i = s;i >= t; i -= p)
#ifdef LOCAL
FILE *InFile = infile("in.in"),*OutFile = outfile("out.out");
// FILE *ErrFile=errfile("err.err");
#else
FILE *Infile = stdin,*OutFile = stdout;
//FILE *ErrFile = stderr;
#endif
using ll=long long;using ull=unsigned long long;
using db = double;using ldb = long double;
const int N = 1e4 + 10;
#define eb emplace_back
vector<int> edge[N];
inline void add(int u,int v){edge[u].eb(v);}
int n,dfn[N],low[N],tim,tot,sta[N],top,bel[N],in[N],out[N];
bitset<N> insta;
void Tarjan(int x){
dfn[x] = low[x] = ++tim;
sta[++top] = x;insta[x] = true;
for(int y:edge[x]){
if(!dfn[y]){
Tarjan(y);
low[x] = min(low[x],low[y]);
}
else if(insta[y]) low[x] = min(low[x],dfn[y]);
}
if(dfn[x] == low[x]){
tot++;int y;
do{
y = sta[top--];
bel[y] = tot;
insta[y] = false;
}while(y != x);
}
}
inline void solve(){
cin>>n;
rep(i,1,n,1){int x; while(cin>>x&&x) add(i,x);}
rep(i,1,n,1) if(!dfn[i]) Tarjan(i);
rep(x,1,n,1) for(int y:edge[x]) if(bel[x] != bel[y]) in[bel[y]]++,out[bel[x]]++;
if(tot == 1) return cout<<"1\n0",void();
int ans1 = 0,ans2 = 0;
rep(i,1,tot,1) if(!in[i]) ans1++;
rep(i,1,tot,1) if(!out[i]) ans2++;
ans2 = max(ans1,ans2);
cout<<ans1<<'\n'<<ans2<<'\n';
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(false);
cout.tie(nullptr)->sync_with_stdio(false);
solve();
}
  1. [APIO2009] 抢掠计划

缩完点后跑最长路就没了

点此查看代码
#include<bits/stdc++.h>
#include<bits/extc++.h>
// using namespace __gnu_pbds;
// using namespace __gnu_cxx;
using namespace std;
#define infile(x) freopen(x,"r",stdin)
#define outfile(x) freopen(x,"w",stdout)
#define errfile(x) freopen(x,"w",stderr)
#define rep(i,s,t,p) for(int i = s;i <= t; i += p)
#define drep(i,s,t,p) for(int i = s;i >= t; i -= p)
#ifdef LOCAL
FILE *InFile = infile("in.in"),*OutFile = outfile("out.out");
// FILE *ErrFile=errfile("err.err");
#else
FILE *Infile = stdin,*OutFile = stdout;
//FILE *ErrFile = stderr;
#endif
using ll=long long;using ull=unsigned long long;
using db = double;using ldb = long double;
const int N = 5e5 + 10;
struct node{int to,w;node(int To,int W):to(To),w(W){}};vector<node> e2[N];
#define eb emplace_back
vector<int> e1[N];
int n,m,s,p,ed[N],a[N],dfn[N],low[N],tim,sta[N],top,g[N],num[N],tot;
int dist[N];
bitset<N> insta,vis;
void Tarjan(int x){
dfn[x] = low[x] = ++tim;
sta[++top] = x;insta[x] = true;
for(int y:e1[x]){
if(!dfn[y]) Tarjan(y),low[x] = min(low[x],low[y]);
else if(insta[y]) low[x] = min(low[x],dfn[y]);
}
if(dfn[x] == low[x]){
tot++;int y;
do{
y = sta[top--];
insta[y] = false;
g[y] = tot;
num[tot] += a[y];
}while(y != x);
}
}
inline void spfa(int s){
queue<int> q;q.push(s);vis[s] = true;
while(q.size()){
int x = q.front();q.pop();vis[x] = false;
for(node i:e2[x]){
int y = i.to,w = i.w;
if(dist[y] < dist[x] + w){
dist[y] = dist[x] + w;
if(!vis[y]) q.push(y),vis[y] = true;
}
}
}
}
inline void solve(){
cin>>n>>m;
rep(i,1,m,1){int u,v;cin>>u>>v;e1[u].eb(v);}
rep(i,1,n,1) cin>>a[i];
rep(i,1,n,1) if(!dfn[i]) Tarjan(i);
rep(x,1,n,1) for(int y:e1[x]) if(g[x] != g[y]) e2[g[x]].eb(node(g[y],num[g[y]]));
cin>>s>>p;rep(i,1,p,1) cin>>ed[i];
spfa(g[s]);
int ans = 0;
rep(i,1,p,1) ans = max(ans,dist[g[ed[i]]]);
cout<<ans+num[g[s]]<<'\n';
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(false);
cout.tie(nullptr)->sync_with_stdio(false);
solve();
}

双连通分量

这里的连通图均指无向连通图

前置知识

割点

对于一个连通图G=(V,E),如果VVG[VV]不连通,则V是图G的一个点割集,当||=1时,称其为割点。

容易发现,孤立点不是割点,孤立边的两个端点也不是割点。

点双连通 : 没有割点的连通图。

点双连通分量(V-BCC) : 一张图的极大点双连通子图,简称点双。

类比割点的定义即可。

对于一个连通图G=(V,E),如果EEG=(V,EE)不连通,则E是图G的一个边割集,当||=1时,称其为桥。

容易发现,孤立边是割边。

边双连通 : 没有桥的连通图。

边双连通分量(E-BCC) : 一张图的极大边双连通子图,简称边双。

割点和桥

既然已经知道了割点和桥是什么,那么如何求呢?

Tarjan求割点

和强连通分量一样,还是维护两个数组dfn,lowdfn的定义同上,low的定义更改为不经过其父亲能达到的最小的时间戳。

开始进行dfs,发现对于大部分时候,ysonxlowydfnx时,x为割点。就是无法不通过x回到祖先。

但是这对于搜索的起点并不成立,所以特殊考虑起点。考虑在搜索树上,如果起点只有一个儿子,那么说明删去这个点并不影响其搜索树子树内的连通性,所以其不是割点。而如果有两个及以上的儿子,那么说明删去以后这个儿子便不连通,所以此时起点也是割点。

image

就比如这张图,容易发现2为图中唯一的一个割点。

如果以1为起点搜索,画出来的搜索树就是这样的

image

而以2为起点搜索,画出来的搜索树是这样的

image

看,很符合上面说的。

【模板】割点(割顶)

点此查看代码
#include<bits/stdc++.h>
#include<bits/extc++.h>
// using namespace __gnu_pbds;
// using namespace __gnu_cxx;
using namespace std;
#define infile(x) freopen(x,"r",stdin)
#define outfile(x) freopen(x,"w",stdout)
#define errfile(x) freopen(x,"w",stderr)
#define rep(i,s,t,p) for(int i = s;i <= t; i += p)
#define drep(i,s,t,p) for(int i = s;i >= t; i -= p)
#ifdef LOCAL
FILE *InFile = infile("in.in"),*OutFile = outfile("out.out");
// FILE *ErrFile=errfile("err.err");
#else
FILE *Infile = stdin,*OutFile = stdout;
//FILE *ErrFile = stderr;
#endif
using ll=long long;using ull=unsigned long long;
using db = double;using ldb = long double;
const int N = 2e4 + 10;
int n,m,rt,dfn[N],low[N],tim;
vector<int> edge[N];
bitset<N> vis,cut;
#define eb emplace_back
void Tarjan(int x,int f){
dfn[x] = low[x] = ++tim;
vis[x] = true;
int son = 0;
for(int y:edge[x]){
if(y == f) continue;
if(!vis[y]){
Tarjan(y,x);son++;
low[x] = min(low[x],low[y]);
if(x == rt && son > 1) cut[x] = true;
if(low[y] >= dfn[x] && x != rt) cut[x] = true;
}
else low[x] = min(low[x],dfn[y]);
}
}
inline void solve(){
cin>>n>>m;
rep(i,1,m,1){int u,v;cin>>u>>v;edge[u].eb(v);edge[v].eb(u);}
rep(i,1,n,1) if(!dfn[i]) rt = i,Tarjan(i,0);
cout<<cut.count()<<'\n';
rep(i,1,n,1) if(cut[i]) cout<<i<<' ';
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(false);
cout.tie(nullptr)->sync_with_stdio(false);
solve();
}

Tarjan求割边

和求割点差不多的,就是将lowydfnx变成lowy>dfnx,而且不需要判断是否为根节点。

【模板】割边(桥)

点此查看代码
#include<bits/stdc++.h>
#include<bits/extc++.h>
// using namespace __gnu_pbds;
// using namespace __gnu_cxx;
using namespace std;
#define infile(x) freopen(x,"r",stdin)
#define outfile(x) freopen(x,"w",stdout)
#define errfile(x) freopen(x,"w",stderr)
#define rep(i,s,t,p) for(int i = s;i <= t; i += p)
#define drep(i,s,t,p) for(int i = s;i >= t; i -= p)
#ifdef LOCAL
FILE *InFile = infile("in.in"),*OutFile = outfile("out.out");
// FILE *ErrFile=errfile("err.err");
#else
FILE *Infile = stdin,*OutFile = stdout;
//FILE *ErrFile = stderr;
#endif
using ll=long long;using ull=unsigned long long;
using db = double;using ldb = long double;
const int N = 1e5 + 10;
int n,m,dfn[N],low[N],tim,cnt;
vector<int> edge[N];
bitset<N> vis;
#define eb emplace_back
void Tarjan(int x,int f){
dfn[x] = low[x] = ++tim;
vis[x] = true;
int son = 0;
for(int y:edge[x]){
if(y == f) continue;
if(!vis[y]){
Tarjan(y,x);son++;
low[x] = min(low[x],low[y]);
if(low[y] > dfn[x]) cnt++;
}
else low[x] = min(low[x],dfn[y]);
}
}
inline void solve(){
cin>>n>>m;
rep(i,1,m,1){int u,v;cin>>u>>v;edge[u].eb(v);edge[v].eb(u);}
rep(i,1,n,1) if(!dfn[i]) Tarjan(i,0);
cout<<cnt;
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(false);
cout.tie(nullptr)->sync_with_stdio(false);
solve();
}

双连通分量

边双连通分量

综合了一下,还是选择了先找出桥后dfs找边双的做法,较为好理解而且不是很容易写挂。

直接写就可以了。建议还是链式前向星建图,比较好判割边。

【模板】边双连通分量

点此查看代码
#include<bits/stdc++.h>
#include<bits/extc++.h>
// using namespace __gnu_pbds;
// using namespace __gnu_cxx;
using namespace std;
#define infile(x) freopen(x,"r",stdin)
#define outfile(x) freopen(x,"w",stdout)
#define errfile(x) freopen(x,"w",stderr)
#define rep(i,s,t,p) for(int i = s;i <= t; i += p)
#define drep(i,s,t,p) for(int i = s;i >= t; i -= p)
#ifdef LOCAL
FILE *InFile = infile("in.in"),*OutFile = outfile("out.out");
// FILE *ErrFile=errfile("err.err");
#else
FILE *Infile = stdin,*OutFile = stdout;
//FILE *ErrFile = stderr;
#endif
using ll=long long;using ull=unsigned long long;
using db = double;using ldb = long double;
const int N = 5e5 + 10,M = 4e6 + 10;
struct EDGE{int to,next;}edge[M];
int head[N],cnt = 1;
inline void add(int u,int v){edge[++cnt] = {v,head[u]};head[u] = cnt;}
vector<int> vdcc[N];int tot;
bitset<M> bri;
bitset<N> vis;
int n,m,dfn[N],low[N],tim;
void Tarjan(int x,int f){
dfn[x] = low[x] = ++tim;
vis[x] = true;
for(int i = head[x]; i;i = edge[i].next){
int y = edge[i].to;
if(y == f) continue;
if(!vis[y]){
Tarjan(y,x);
low[x] = min(low[x],low[y]);
if(low[y] > dfn[x]) bri[i] = bri[i^1] = true;
}
else low[x] = min(low[x],dfn[y]);
}
}
void dfs(int x,int f){
vis[x] = true;
vdcc[tot].emplace_back(x);
for(int i = head[x]; i;i = edge[i].next){
int y = edge[i].to;
if(bri[i] || y == f || vis[y]) continue;
dfs(y,x);
}
}
inline void solve(){
cin>>n>>m;
rep(i,1,m,1){int u,v;cin>>u>>v;add(u,v);add(v,u);}
rep(i,1,n,1) if(!dfn[i]) Tarjan(i,0);
vis.reset();
rep(i,1,n,1) if(!vis[i]) tot++,dfs(i,0);
cout<<tot<<'\n';
rep(i,1,tot,1){cout<<vdcc[i].size()<<' ';for(int j:vdcc[i]) cout<<j<<' ';cout<<'\n';}
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(false);
cout.tie(nullptr)->sync_with_stdio(false);
solve();
}

点双连通分量

对于一个点双,它在dfs搜索树中dfn值最小的点一定为割点或树根。

分类讨论:

  1. 这个点为割点,那么它就是点双的根
  2. 这个点为树根
    1. 有两个及以上子树,为一个割点
    2. 只有一个子树,它是一个点双的根
    3. 没有子树,视为一个点双。
点此查看代码
#include<bits/stdc++.h>
#include<bits/extc++.h>
// using namespace __gnu_pbds;
// using namespace __gnu_cxx;
using namespace std;
#define infile(x) freopen(x,"r",stdin)
#define outfile(x) freopen(x,"w",stdout)
#define errfile(x) freopen(x,"w",stderr)
#define rep(i,s,t,p) for(int i = s;i <= t; i += p)
#define drep(i,s,t,p) for(int i = s;i >= t; i -= p)
#ifdef LOCAL
FILE *InFile = infile("in.in"),*OutFile = outfile("out.out");
// FILE *ErrFile=errfile("err.err");
#else
FILE *Infile = stdin,*OutFile = stdout;
//FILE *ErrFile = stderr;
#endif
using ll=long long;using ull=unsigned long long;
using db = double;using ldb = long double;
const int N = 5e5 + 10;
vector<int> edge[N],vdcc[N];
#define eb emplace_back
int n,m,rt,dfn[N],low[N],tim,sta[N],top,tot,d[N];
bitset<N> cut;
void Tarjan(int x,int f){
dfn[x] = low[x] = ++tim;
sta[++top] = x;int son = 0;
for(int y:edge[x]){
if(!dfn[y]){
Tarjan(y,x);low[x] = min(low[x],low[y]);
if(dfn[x] <= low[y]){
son++;
if(x != rt || son > 1) cut[x] = true;
tot++;int z;
do{
z = sta[top--];
vdcc[tot].eb(z);
}while(z != y);
vdcc[tot].eb(x);
}
}
else low[x] = min(low[x],dfn[y]);
}
if(x == rt && !son) return vdcc[++tot].eb(x),void();
}
inline void solve(){
cin>>n>>m;
rep(i,1,m,1){int u,v;cin>>u>>v;edge[u].eb(v);edge[v].eb(u);}
rep(i,1,n,1) if(!dfn[i]) rt = i,Tarjan(i,0);
cout<<tot<<'\n';
rep(i,1,tot,1){
cout<<vdcc[i].size()<<' ';
for(int j:vdcc[i]) cout<<j<<' ';
cout<<'\n';
}
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(false);
cout.tie(nullptr)->sync_with_stdio(false);
solve();
}

2-SAT

定义

就是给出n个集合,每个集合有两个元素{a,b},表示a,b矛盾,然后从每个集合中选择一个元素,问是否可以选出n个两两互不矛盾的元素。

解决方法

如果a,b矛盾,那么¬a,b不矛盾,¬b,a不矛盾,将这两对连边。

然后跑一边缩点,判断是否有a,b或者¬a,¬b在同一SCC中

输出方案时可以通过变量在图中的拓扑序确定该变量的取值。如果变量 x 的拓扑序在 ¬x 之后,那么取 x 值为真。应用到 Tarjan 算法的缩点,即 x 所在 SCC 编号在 ¬x 之前时,取 x 为真。因为 Tarjan 算法求强连通分量时使用了栈,所以 Tarjan 求得的 SCC 编号相当于反拓扑序。

【模板】2-SAT

分为四种情况讨论建图

  1. i为真j也为真 :¬ab,¬ba
  2. i为真j为假 : ¬a¬b,ba
  3. i为假j为真 : ab,¬b¬a
  4. i为假j也为假 : a¬b,b¬a

然后对12×n跑Tarjan,如果i[1,n],ii+n在同一个强连通分量中,说明无解。

反之,输出解,当coli<coli+n时,值取true,反之,取false。

点此查看代码
#include<bits/stdc++.h>
#include<bits/extc++.h>
// using namespace __gnu_pbds;
// using namespace __gnu_cxx;
using namespace std;
#define infile(x) freopen(x,"r",stdin)
#define outfile(x) freopen(x,"w",stdout)
#define errfile(x) freopen(x,"w",stderr)
#define rep(i,s,t,p) for(int i = s;i <= t; i += p)
#define drep(i,s,t,p) for(int i = s;i >= t; i -= p)
#ifdef LOCAL
FILE *InFile = infile("in.in"),*OutFile = outfile("out.out");
// FILE *ErrFile=errfile("err.err");
#else
FILE *Infile = stdin,*OutFile = stdout;
//FILE *ErrFile = stderr;
#endif
using ll=long long;using ull=unsigned long long;
using db = double;using ldb = long double;
const int N = 2e6 + 10;
vector<int> edge[N];
#define eb emplace_back
inline void add(int u,int v){edge[u].eb(v);}
int n,m,dfn[N],low[N],tim,g[N],sta[N],top,num;
bitset<N> insta;
void Tarjan(int x){
dfn[x] = low[x] = ++tim;
sta[++top] = x;insta[x] = true;
for(int y:edge[x]){
if(!dfn[y]) {Tarjan(y);low[x] = min(low[x],low[y]);}
else if(insta[y]) low[x] = min(low[x],dfn[y]);
}
if(dfn[x] == low[x]){
num++;int y;
do{
y = sta[top--];
insta[y] = false;
g[y] = num;
}while(y != x);
}
}
inline void solve(){
cin>>n>>m;
rep(i,1,m,1){
int x,a,y,b;
cin>>x>>a>>y>>b;
if(a&&b) add(x+n,y),add(y+n,x);
if(a&&!b) add(x+n,y+n),add(y,x);
if(!a&&b) add(x,y),add(y+n,x+n);
if(!a&&!b) add(x,y+n),add(y,x+n);
}
rep(i,1,n<<1,1) if(!dfn[i]) Tarjan(i);
rep(i,1,n,1) if(g[i] == g[i+n]) cout<<"IMPOSSIBLE\n",exit(0);
cout<<"POSSIBLE\n";
rep(i,1,n,1) cout<<(bool)(g[i] < g[i+n])<<' ';
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(false);
cout.tie(nullptr)->sync_with_stdio(false);
solve();
}

练习 :

  1. [JSOI2010] 满汉全席

还是那么套路,直接套上就好啦。

点此查看代码
#include<bits/stdc++.h>
#include<bits/extc++.h>
// using namespace __gnu_pbds;
// using namespace __gnu_cxx;
using namespace std;
#define infile(x) freopen(x,"r",stdin)
#define outfile(x) freopen(x,"w",stdout)
#define errfile(x) freopen(x,"w",stderr)
#define rep(i,s,t,p) for(int i = s;i <= t; i += p)
#define drep(i,s,t,p) for(int i = s;i >= t; i -= p)
#ifdef LOCAL
FILE *InFile = infile("in.in"),*OutFile = outfile("out.out");
// FILE *ErrFile=errfile("err.err");
#else
FILE *Infile = stdin,*OutFile = stdout;
//FILE *ErrFile = stderr;
#endif
using ll=long long;using ull=unsigned long long;
using db = double;using ldb = long double;
const int N = 210;
vector<int> edge[N];//1~n 为满
#define eb emplace_back
int n,m,dfn[N],low[N],sta[N],top,tim,tot,g[N];
bitset<N> insta;
void Tarjan(int x){
dfn[x] = low[x] = ++tim;
sta[++top] = x;insta[x] = true;
for(int y:edge[x]){
if(!dfn[y]) Tarjan(y),low[x] = min(low[x],low[y]);
else if(insta[y]) low[x] = min(low[x],dfn[y]);
}
if(dfn[x] == low[x]){
tot++;int y;
do{
y = sta[top--];
insta[y] = false;
g[y] = tot;
}while(y != x);
}
}
inline void solve(){
cin>>n>>m;
tim = top = tot = 0;
rep(i,1,n<<1,1) dfn[i] = low[i] = insta[i] = g[i] = false;
rep(i,1,n<<1,1) vector<int> ().swap(edge[i]);
rep(i,1,m,1){
char x,y;int id1,id2;
cin>>x>>id1>>y>>id2;
if(x == 'm'){
if(y == 'm') edge[id1+n].eb(id2),edge[id2+n].eb(id1);
else edge[id1+n].eb(id2+n),edge[id2].eb(id1);
}
else{
if(y == 'm') edge[id1].eb(id2),edge[id2+n].eb(id1+n);
else edge[id1].eb(id2+n),edge[id2].eb(id1+n);
}
}
rep(i,1,n<<1,1) if(!dfn[i]) Tarjan(i);
bool flag = false;
rep(i,1,n,1) if(g[i] == g[i+n]){flag = true;break;}
cout<<(flag?"BAD\n":"GOOD\n");
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(false);
cout.tie(nullptr)->sync_with_stdio(false);
int T;cin>>T;while(T--)solve();
}
  1. [POI2001] 和平委员会

还是建反边跑Tarjan

点此查看代码
#include<bits/stdc++.h>
#include<bits/extc++.h>
// using namespace __gnu_pbds;
// using namespace __gnu_cxx;
using namespace std;
#define infile(x) freopen(x,"r",stdin)
#define outfile(x) freopen(x,"w",stdout)
#define errfile(x) freopen(x,"w",stderr)
#define rep(i,s,t,p) for(int i = s;i <= t; i += p)
#define drep(i,s,t,p) for(int i = s;i >= t; i -= p)
#ifdef LOCAL
FILE *InFile = infile("in.in"),*OutFile = outfile("out.out");
// FILE *ErrFile=errfile("err.err");
#else
FILE *Infile = stdin,*OutFile = stdout;
//FILE *ErrFile = stderr;
#endif
using ll=long long;using ull=unsigned long long;
using db = double;using ldb = long double;
const int N = 20010;
#define eb emplace_back
vector<int> edge[N];
inline void add(int u,int v){edge[u].eb(v);}
int n,m,dfn[N],low[N],tim,num,g[N],sta[N],top;
bitset<N> insta;
void Tarjan(int x){
dfn[x] = low[x] = ++tim;
sta[++top] = x;insta[x] = true;
for(int y:edge[x]){
if(!dfn[y]) Tarjan(y),low[x] = min(low[x],low[y]);
else if(insta[y]) low[x] = min(low[x],dfn[y]);
}
if(dfn[x] == low[x]){
num++;int y;
do{
y = sta[top--];
insta[y] = false;
g[y] = num;
}while(y != x);
}
}
inline void solve(){
cin>>n>>m;
auto opp = [](int x){return (x&1)?x+1:x-1;};
rep(i,1,m,1){
int a,b;cin>>a>>b;
add(a,opp(b));add(b,opp(a));
}
rep(i,1,n<<1,1) if(!dfn[i]) Tarjan(i);
rep(i,1,n<<1,2) if(g[i] == g[i+1]) cout<<"NIE\n",exit(0);
rep(i,1,n<<1,2) cout<<(g[i]<g[i+1]?i:i+1)<<'\n';
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(false);
cout.tie(nullptr)->sync_with_stdio(false);
solve();
}
  1. Catowice City

好题。

考虑到选了第i个人就不能选择第i个人所认识的猫,所以将第i个人与其所认识的猫的主人连边,求强连通分量。

考虑到选人最好的方法就是选择一个没有出度的强连通分量,这样就可以保证直接选人即可,不用再考虑其它的限制。

深入理解一下Tarjan算法,发现找到的第一个强连通分量便没有出度。因为如果有出度的话,那么Tarjan算法就会递归进入到下一个强连通分量中,直到一个没有出度的强连通分量为止。

选的人的个数就是第一个强连通分量的个数,人就是强连通分量中的人,剩下的猫都是选手。

特判只有一个强连通分量时,无解。

点此查看代码
#include<bits/stdc++.h>
#include<bits/extc++.h>
// using namespace __gnu_pbds;
// using namespace __gnu_cxx;
using namespace std;
#define infile(x) freopen(x,"r",stdin)
#define outfile(x) freopen(x,"w",stdout)
#define errfile(x) freopen(x,"w",stderr)
#define rep(i,s,t,p) for(int i = s;i <= t; i += p)
#define drep(i,s,t,p) for(int i = s;i >= t; i -= p)
#ifdef LOCAL
FILE *InFile = infile("in.in"),*OutFile = outfile("out.out");
// FILE *ErrFile=errfile("err.err");
#else
FILE *Infile = stdin,*OutFile = stdout;
//FILE *ErrFile = stderr;
#endif
using ll=long long;using ull=unsigned long long;
using db = double;using ldb = long double;
const int N = 1e6 + 10;
#define eb emplace_back
#define All(x) x.begin(),x.end()
vector<int> edge[N];
inline void add(int u,int v){edge[u].eb(v);}
int n,m,dfn[N],low[N],tim,num,g[N],sta[N],top,len[N];
bitset<N> insta;
void Tarjan(int x){
dfn[x] = low[x] = ++tim;
sta[++top] = x;insta[x] = true;
for(int y:edge[x]){
if(!dfn[y]) Tarjan(y),low[x] = min(low[x],low[y]);
else if(insta[y]) low[x] = min(low[x],dfn[y]);
}
if(dfn[x] == low[x]){
num++;int y;
do{
y = sta[top--];
insta[y] = false;
g[y] = num;len[num]++;
}while(y != x);
}
}
inline void solve(){
cin>>n>>m;
rep(i,1,num,1) len[i] = 0;
tim = top = num = 0;
rep(i,1,n,1) vector<int>().swap(edge[i]),dfn[i] = low[i] = g[i] = insta[i] = false;
rep(i,1,m,1){
int a,b;cin>>a>>b;
if(a == b) continue;
add(a,b);
}
rep(i,1,n,1) if(!dfn[i]) Tarjan(i);
if(num == 1) return cout<<"No\n",void();
cout<<"Yes\n"<<len[1]<<' '<<n-len[1]<<'\n';
rep(i,1,n,1) if(g[i] == 1) cout<<i<<' ';
cout<<'\n';
rep(i,1,n,1) if(g[i] != 1) cout<<i<<' ';
cout<<'\n';
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(false);
cout.tie(nullptr)->sync_with_stdio(false);
int T;cin>>T;while(T--) solve();
}

好像还有一个圆方树完了,到时候再开一个写

posted @   CuFeO4  阅读(26)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示