强连通分量
强连通分量是针对有向图来说的,当一个有向图中所有的点都能够相互到达则称这个图为强连通分量。
一、模板
int dfn[N], low[N], dfncnt, s[N], in_stack[N], tp;
int scc[N], sc; // 结点 i 所在 SCC 的编号
int sz[N]; // 强连通 i 的大小
void tarjan(int u) {
low[u] = dfn[u] = ++dfncnt, s[++tp] = u, in_stack[u] = 1;
for (int i = h[u]; i; i = e[i].nex) {
const int &v = e[i].t;
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
} else if (in_stack[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u]) {
++sc;
while (s[tp] != u) {
scc[s[tp]] = sc;
sz[sc]++;
in_stack[s[tp]] = 0;
--tp;
}
scc[s[tp]] = sc;
sz[sc]++;
in_stack[s[tp]] = 0;
--tp;
}
}
参考代码:
#include <iostream>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <vector>
#include <map>
#include <queue>
#define int long long
using namespace std;
typedef pair<int, int> PII;
const int N = 1e4 + 10;
int n, m, ans = -1;
int w[N];
int dfn[N], low[N], timetamp;
int stk[N], top;
bool st[N], stt[N];
int id[N];
int scc_cnt;
int res[N], dist[N];
int d[N];
vector<int> g[N];
vector<int> h[N];
map<PII, bool> p;
void tarjan(int u)
{
dfn[u] = low[u] = ++timetamp;
stk[++ top] = u;
st[u] = true;
for (int i : g[u])
{
if (!dfn[i])
{
tarjan(i);
low[u] = min(low[u], low[i]);
}
else if (st[i])
{
low[u] = min(low[u], dfn[i]);
}
}
if (low[u] == dfn[u])
{
int y;
++ scc_cnt;
do
{
y = stk[top--];
st[y] = false;
id[y] = scc_cnt;
res[scc_cnt] += w[y];
} while (y != u);
}
}
void topsort()
{
queue<int> q;
for (int i = 1; i <= scc_cnt; i++)
{
if (d[i] == 0)
{
q.push(i);
dist[i] = res[i];
}
}
while (q.size())
{
int t = q.front();
q.pop();
for (int i : h[t])
{
dist[i] = max(dist[i], dist[t] + res[i]);
d[i]--;
if (d[i] == 0)
{
q.push(i);
}
}
}
for (int i = 1; i <= scc_cnt; i++)
{
ans = max(ans, dist[i]);
}
}
signed main()
{
scanf("%lld%lld", &n, &m);
for (int i = 1; i <= n; i++)
{
scanf("%lld", &w[i]);
}
for (int i = 0; i < m; i++)
{
int a, b;
scanf("%lld%lld", &a, &b);
g[a].push_back(b);
}
for (int i = 1; i <= n; i++)
{
if (!dfn[i])
{
tarjan(i);
}
}
for (int i = 1; i <= n; i++)
{
for (int j : g[i])
{
if (id[i] != id[j])
{
h[id[i]].push_back(id[j]);
d[id[j]]++;
}
}
}
/*
for(int i = 1; i <= scc_cnt; i ++){
for(int j : h[i]){
printf("%d->%d\n",i , j);
}
}*/
topsort();
printf("%lld\n", ans);
return 0;
}
二、应用
思路: 当一群牛相互爱慕时,我们可以用tarjan 算法将其缩成一个点。一群牛受欢迎当且仅当其余所有牛都爱慕他,即其余所有点都能够到达他,出度为0, 若缩点之后的图中存在两个及以上的点出度为0时则没有牛是受欢迎的,因为至少有一头别的牛不认为他受欢迎。
参考代码:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 1e4 + 10;
int n, m;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool st[N];
int id[N], scc_cnt, scc_size[N];
int dout[N];
vector<int> g[N];
void tarjan(int u){
dfn[u] = low[u] = ++ timestamp;
stk[++ top] = u;
st[u] = true;
for(int son : g[u]){
if(!dfn[son]){
tarjan(son);
low[u] = min(low[u], low[son]);
}
else if(st[son]){
low[u] = min(low[u], dfn[son]);
}
}
if(dfn[u] == low[u]){
++scc_cnt;
int y;
do{
y = stk[top --];
st[y] = false;
id[y] = scc_cnt;
scc_size[scc_cnt] ++;
}while(y != u);
}
}
int main(){
scanf("%d%d",&n, &m);
for(int i = 0; i < m ; i ++){
int a, b;
scanf("%d%d",&a, &b);
g[a].push_back(b);
}
for(int i = 1; i <= n ; i ++){
if(!dfn[i]){
tarjan(i);
}
}
for(int i = 1; i <= n ; i ++){
for(int j: g[i]){
int a = id[i];
int b = id[j];
if(a != b){
dout[a] ++;
}
}
}
int zeors = 0, sum = 0;
for(int i = 1; i <= scc_cnt; i++){
if(!dout[i]){
zeors ++;
sum += scc_size[i];
if(zeors > 1){
sum = 0;
break;
}
}
}
printf("%d\n", sum);
return 0;
}
很裸的强连通分量问题,直接套模板即可。
参考代码:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<vector>
using namespace std;
const int N = 1e5 + 10;
int dfn[N], low[N], timetamp;
int stk[N], top;
bool st[N];
int n, m, ans;
int scc_cnt, scc_size[N];
int id[N];
vector<int> g[N];
void tarjan(int u){
dfn[u] = low[u] = ++ timetamp;
stk[++ top] = u;
st[u] = true;
for(int i : g[u]){
if(!dfn[i]){
tarjan(i);
low[u] = min(low[u], low[i]);
}
else if(st[i]){
low[u] = min(low[u], dfn[i]);
}
}
if(low[u] == dfn[u]){
int y;
++ scc_cnt;
do{
y = stk[top --];
st[y] = false;
id[y] = scc_cnt;
scc_size[scc_cnt] ++;
}while(y != u);
}
}
int main(){
scanf("%d%d",&n, &m);
for(int i = 0; i < m ; i ++){
int a, b;
scanf("%d%d",&a, &b);
g[a].push_back(b);
}
for(int i = 1; i <= n ; i ++){
if(!dfn[i]){
tarjan(i);
}
}
for(int i = 1; i <= scc_cnt; i ++){
if(scc_size[i] > 1){
ans ++;
}
}
printf("%d\n", ans);
return 0;
}
思路: 由题意可得所给接收学校关系为一条有向边。首先将原图通过tarjan缩点得到有向无环图,统计每个强连通分量的出度入度,起点数量为 a,终点数量为 b。对于一个强连通分量,其中只要有一所学校获得新软件那么整个分量都能获得。对于任务A,只需要把软件交给这a个起点学校就能保证所有学校都接收到软件。对于任务B,我们只需将b个终点与a个起点随意相连就能将整张图强连通,所以答案为max(a, b), 当然若原图本来就是一个强连通分量则不再需要添加边,答案为0。
参考代码:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<vector>
using namespace std;
const int N = 110;
vector<int> g[N];
int dfn[N], low[N], timestamp;
int stk[N], top;
int din[N], dout[N];
int id[N], n;
int scc_cnt, scc_size[N];
bool st[N];
void tarjan(int u){
dfn[u] = low[u] = ++timestamp;
stk[++ top] = u;
st[u] = true;
for(int j : g[u]){
if(!dfn[j]){
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if(st[j]){
low[u] = min(low[u], dfn[j]);
}
}
if(dfn[u] == low[u]){
++ scc_cnt;
int y;
do{
y = stk[top --];
st[y] = false;
id[y] = scc_cnt;
scc_size[scc_cnt] ++;
}while(y != u);
}
}
int main(){
scanf("%d",&n);
for(int i = 1; i <= n ; i ++){
int x;
while(~scanf("%d",&x)){
if(x == 0){
break;
}
g[i].push_back(x);
}
}
for(int i = 1; i <= n ;i ++){
if(!dfn[i]){
tarjan(i);
}
}
for(int i = 1; i <= n ; i ++){
for(int j : g[i]){
int a = id[i];
int b = id[j];
if(a != b){
dout[a] ++;
din[b] ++;
}
}
}
int a = 0, b = 0;
for(int i = 1; i <= scc_cnt; i ++){
if(!dout[i]){
a ++;
}
if(!din[i]){
b ++;
}
}
printf("%d\n", b);
if(scc_cnt == 1){
printf("0\n");
}
else{
printf("%d\n", max(a, b));
}
return 0;
}
思路: 首先对有向图进行tarjan缩点,得到有向无环图。缩点之后的所有点是强连通分量,满足半连通性质,再在缩点之后的拓扑图中找到最长连,记录所有点数即可。
参考代码:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
#include<unordered_set>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10, M = 3e6 + 10;
int stk[N], top;
int st[N], in_stk[N];
int scc_size[N], scc_cnt;
int id[N];
int n, m, mod;
int f[N], g[N];
int dfn[N], low[N], timestamp;
int h1[N], h2[N], e[M], ne[M], idx;
void add(int h[], int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
void tarjan(int u){
dfn[u] = low[u] = ++ timestamp;
stk[++ top] = u;
in_stk[u] = true;
for(int i = h1[u]; i != -1; i = ne[i]){
int j = e[i];
if(!dfn[j]){
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if(in_stk[j]){
low[u] = min(low[u], dfn[j]);
}
}
if(low[u] == dfn[u]){
int y;
++ scc_cnt;
do{
y = stk[top --];
in_stk[y] = false;
scc_size[scc_cnt] ++;
id[y] = scc_cnt;
}while(y != u);
}
}
int main(){
memset(h1, -1, sizeof(h1));
memset(h2, -1,sizeof(h2));
scanf("%d%d%d",&n, &m, &mod);
for(int i = 0; i < m ; i ++){
int a, b;
scanf("%d%d",&a, &b);
add(h1, a, b);
}
for(int i = 1; i <= n; i ++){
if(!dfn[i]){
tarjan(i);
}
}
unordered_set<LL> p;
for(int i = 1; i <= n ; i ++){
for(int j = h1[i]; j != -1; j = ne[j]){
int k = e[j];
int a = id[i];
int b = id[k];
if(a != b){
int hash = a * 1000000ll + b;
if(!p.count(hash)){
p.insert(hash);
add(h2, a, b);
}
}
}
}
for(int i = scc_cnt; i >= 0; i --){
if(!f[i]){
f[i] = scc_size[i];
g[i] = 1;
}
for(int j = h2[i]; j != -1; j = ne[j]){
int k = e[j];
if(f[k] < f[i] + scc_size[k]){
f[k] = f[i] + scc_size[k];
g[k] = g[i];
}
else if(f[k] == f[i] + scc_size[k]){
g[k] = (g[k] + g[i]) % mod;
}
}
}
int maxf = -1, sum = 0;
for(int i = 1; i <= scc_cnt; i ++){
if(f[i] > maxf){
maxf = f[i];
sum = g[i];
}
else if(f[i] == maxf ){
sum = (sum + g[i]) % mod;
}
}
printf("%d\n%d\n",maxf, sum);
return 0;
}
思路: 乍一看很像差分约束,但是当差分约束中所有边权为整数时可以通过缩点求解,进一步简化,若所有边权为1时可以直接用拓扑排序求解。首先根据差分约束的思路建图,在图上跑tarjan缩点,若得到的拓扑图中的某一连通分量中有一个边权大于0的边时,则没有答案。否则按照拓扑序逆序求最最短路(所有连通分量的距离的初始值为1,因为每个小朋友至少要分到1个糖果), 跑完一边最短路之后求和即为答案。
参考代码:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#define int long long
using namespace std;
const int N = 1e5 + 10, M = 6e5 +10;
int h1[N], h2[N], e[M], ne[M], w[M], idx;
int stk[N], top;
bool st[N];
int dfn[N], low[N], timetamp;
int id[N];
int n, m;
int f[N], g[N], dist[N];
int scc_cnt, scc_size[N];
void add(int h[],int a, int b, int c){
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
void tarjan(int u){
dfn[u] = low[u] = ++ timetamp;
stk[++ top] = u;
st[u] = true;
for(int i = h1[u]; i != -1; i = ne[i]){
int j = e[i];
if(!dfn[j]){
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if(st[j]){
low[u] = min(low[u], dfn[j]);
}
}
if(dfn[u] == low[u]){
int y;
++ scc_cnt;
do{
y = stk[top --];
st[y] = false;
id[y] = scc_cnt;
scc_size[scc_cnt] ++;
}while(y != u);
}
}
signed main(){
memset(h1, -1, sizeof(h1));
memset(h2, -1, sizeof(h2));
scanf("%lld%lld",&n, &m);
for(int i = 0; i < m ; i ++){
int a, b, x;
scanf("%lld%lld%lld",&x, &a, &b);
if(x == 1){
add(h1, a, b, 0);
add(h1, b, a, 0);
}
else if(x == 2){
add(h1, a, b, 1);
}
else if(x == 3){
add(h1, b, a, 0);
}
else if(x == 4){
add(h1, b, a, 1);
}
else{
add(h1, a, b, 0);
}
}
for(int i = 1; i <= n ; i ++){
add(h1, 0, i, 1);
}
tarjan(0);
bool flag = true;
for(int i = 0; i <= n ; i ++){
for(int j = h1[i]; j != -1; j = ne[j]){
int k = e[j];
int a = id[i];
int b = id[k];
if(a == b){
if(w[j] > 0){
flag = false;
break;
}
}
else{
add(h2, a, b, w[j]);
}
}
if(!flag){
break;
}
}
if(!flag){
printf("-1\n");
}
else{
for(int i = scc_cnt; i > 0; i --){
for(int j = h2[i]; j != -1; j = ne[j]){
int k = e[j];
dist[k] = max(dist[k], dist[i] + w[j]);
}
}
/*
printf("%d\n", scc_cnt);
for(int i = 1; i <= scc_cnt; i ++){
printf("%lld ", dist[i]);
}*/
int ans = 0;
for(int i = 1; i <= scc_cnt; i ++){
ans += (long long)dist[i] * scc_size[i];
}
printf("%lld\n", ans);
}
}
思路:
对于任意两个选手i, j, 若 i 在两场比赛中的至少一场得分大于 j 就向连 j 连一条边,后进行tarjan缩点,统计每个点的出入度。入度为0的所有点中的所有选手均有可能赢得比赛。
参考代码:
#include <iostream>
#include <cstring>
#include <vector>
#include <cstdio>
#include <algorithm>
#define int long long
using namespace std;
const int N = 1e6 + 10;
typedef pair<int, int> PII;
struct node
{
int x, id;
bool operator<(const node &t) const
{
return x < t.x;
}
} a[N];
vector<int> g[N];
int n;
int dfn[N], low[N], timetamp;
int stk[N], top;
int d[N];
int id[N], scc_cnt;
bool st[N];
void tarjan(int u)
{
dfn[u] = low[u] = ++timetamp;
stk[++top] = u;
st[u] = true;
for (int son : g[u])
{
if (!dfn[son])
{
tarjan(son);
low[u] = min(low[u], low[son]);
}
else if (st[son])
{
low[u] = min(low[u], dfn[son]);
}
}
if (dfn[u] == low[u])
{
++scc_cnt;
int y;
do
{
y = stk[top--];
st[y] = false;
id[y] = scc_cnt;
} while (y != u);
}
}
signed main()
{
int T;
scanf("%lld", &T);
while (T--)
{
scanf("%lld", &n);
for(int i = 0; i <= n ; i ++){
g[i].clear();
}
memset(dfn, 0, sizeof(dfn));
memset(st, false, sizeof(st));
memset(d, 0, sizeof(d));
memset(id, 0, sizeof(id));
memset(low, 0, sizeof(low));
timetamp = top = scc_cnt = 0;
for (int j = 0; j < 2; j++)
{
for (int i = 1; i <= n; i++)
{
scanf("%lld", &a[i].x);
a[i].id = i;
}
sort(a + 1, a + 1 + n);
for (int i = 2; i <= n; i++)
{
g[a[i].id].emplace_back(a[i - 1].id);
}
}
for (int i = 1; i <= n; i++)
{
if (!dfn[i])
{
tarjan(i);
}
}
for (int i = 1; i <= n; i++)
{
for (int j : g[i])
{
if (id[i] != id[j])
{
d[id[j]]++;
}
}
}
for (int i = 1; i <= n; i++)
{
if (d[id[i]])
{
printf("0");
}
else
{
printf("1");
}
}
printf("\n");
}
}