算法笔记-联通性相关(Tarjan)

相关概念

  • 图 : 由点和边组成的集合。
  • 有(无)向图 : 点与点之间状态的转移具有(没有)方向性。
  • 连通 : 如果两个点之间相互可通过路径到达,则称两点连通。
  • 连通分量 : 无向图中极大的任意两点相互连通的子图。
  • 强连通分量 : 有向图中极大的任意两点相互连通的子图。
  • (强)连通图 : 只有一个(强)连通分量的图。
  • 树 : 无向无环图。
  • DAG : 有向无环图。

Tarjan

概况

  • 处理连通性问题的算法。

u 子树:即以 u 为根的子树。

过程

以求强连通分量为例。

Luogu P3387
给定一个 n 个点 m 条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。

容易发现,对于一个强连通分量内的点,我们可以看做一个点来处理,且权值等于其中所有点权值之和。
并且,如果我们按照强连通分量来缩点,我们将得到一个 DAG,然后 DAGTopo 求解最大权值路径。
我们来看其中的缩点部分。

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

dfs 中途维护三个东西:
栈 : 被搜到却没有归属任何一个强连通分量的点。
dfn[u] : udfs 序。
low[u] : 从 u 出发能到达的栈中的点 dfn 的最小值。

过程:
对于 Tarjan(u),有如下过程 :

  • 加入栈。
  • 遍历每一条出边,对于下一个点 v,有以下讨论 :
    • 如果未搜到过, Tarjan(v), 并用 low[v] 更新 low[u]
    • 如果搜到过,并且在栈中,用 dfn[v] 更新 low[u]
    • 否则不作处理。
  • 如果 dfn[u]=low[u],即 u 为某一个强连通分量的根,则将此刻栈中 u 即其上的点全部划为一个强连通分量 scc

反证法证明略。

scc 之间连边建新图跑 Topo 即可。

点击查看

#include <bits/stdc++.h>
#define lep(i, a, b) for (int i = a; i <= b; ++i)
#define rep(i, a, b) for (int i = a; i >= b; --i)

const int _ = 1e4 + 7;

int n, m, scc, bel[_], stk[_], dfn[_], low[_], idx, val[_], _val[_], f[_], deg[_]; bool in[_];
int u, v, ans;
std::vector <int> e[_], _e[_];

void Tarjan(int u) {
    low[u] = dfn[u] = ++idx, stk[++*stk] = u, in[u] = true;
    for (int v : e[u]) {
        if (!dfn[v]) Tarjan(v), low[u] = std::min(low[u], low[v]);
        else if (in[v]) low[u] = std::min(low[u], dfn[v]);
    }
    if (low[u] == dfn[u]) { ++scc;
        while (stk[*stk] != u) {
            int nw = stk[(*stk)--]; in[nw] = false;
            bel[nw] = scc, _val[scc] += val[nw];
        }
        --*stk, bel[u] = scc, _val[scc] += val[u], in[u] = false;
    }
}

int main() {
    scanf("%d%d", & n, & m);
    lep(i, 1, n) scanf("%d", val + i);
    lep(i, 1, m) scanf("%d%d", & u, & v), e[u].push_back(v);
    
    lep(i, 1, n) if (!dfn[i]) Tarjan(i);
    lep(u, 1, n) for (int v : e[u]) if (bel[u] != bel[v])  _e[bel[u]].push_back(bel[v]), ++deg[bel[v]];
    
    std::queue <int> d;
    lep(i, 1, scc) if (!deg[i]) d.push(i);
    while (!d.empty()) {
        int u = d.front(); d.pop(); f[u] += _val[u]; ans = std::max(ans, f[u]);
        for (int v : _e[u]) {
            --deg[v], f[v] = std::max(f[v], f[u]);
            if (!deg[v]) d.push(v);
        }
    }
    
    printf("%d\n", ans);
    return 0;
}

其他

边双连通分量

点击查看

可以发现相当于将无向图的边赋予一个方向后缩点。


#include <bits/stdc++.h>
#define lep(i, a, b) for (int i = a; i <= b; ++i)
#define rep(i, a, b) for (int i = a; i >= b; --i)
#define ep(i, u) for (int i = H[u], v = e[i].v; i ; i = e[i].n, v = e[i].v)

typedef long long ll;
const int _ = 5e5 + 7 ;
const int __ = 4e6 + 7;

struct edge { int v, n; } e[__]; int cnte = 1, H[_];
int n, m, low[_], dfn[_], idx, u, v, stk[_]; bool vis[__];
std::vector <std::vector<int> > Ans;

void A(int u, int v) { e[++cnte] = { v, H[u] }; H[u] = cnte; }
void Tarjan(int u) {
    low[u] = dfn[u] = ++idx, stk[++*stk] = u;
    ep(i, u) {
        if (vis[i ^ 1]) continue; vis[i] = true;
        if (!dfn[v]) Tarjan(v), low[u] = std::min(low[u], low[v]);
        else low[u] = std::min(low[u], dfn[v]);
    }
    if (low[u] == dfn[u]) {
        std::vector <int> x;
        while (stk[*stk] != u) x.push_back(stk[(*stk)--]);
        x.push_back(stk[(*stk)--]);
        Ans.push_back(x);
    }
}

int main() {
    scanf("%d%d", & n, & m);
    lep(i, 1, m) scanf("%d%d", & u, & v), A(u, v), A(v, u);
    lep(i, 1, n) if (!dfn[i]) Tarjan(i);
    
    printf("%d\n", (int)Ans.size());
    for (auto x : Ans) {
        printf("%d\n", (int)x.size());
        for (int i : x) printf("%d ", i);
        putchar('\n');
    }
    return 0;
}


割点

点击查看

对于非根结点 u,可以发现 u 是缩点的充分条件是存在一个子结点 v 满足 low[v] >= dfn[u]
对于根节点 rt,如果 rt 有多于一个子结点,那么 rt 是割点。


#include <bits/stdc++.h>
#define lep(i, a, b) for (int i = a; i <= b; ++i)
#define rep(i, a, b) for (int i = a; i >= b; --i)
#define ep(i, u) for (int i = H[u], v = e[i].v; i ; i = e[i].n, v = e[i].v)

const int _ = 2e4 + 7 ;
const int __ = 2e5 + 7;

struct edge { int v, n; } e[__]; int cnte = 1, H[_];
int n, m, low[_], dfn[_], idx, u, v, rt; bool vis[__], f[_];

void A(int u, int v) { e[++cnte] = { v, H[u] }; H[u] = cnte; }
void Tarjan(int u) { int son = 0;
    low[u] = dfn[u] = ++idx;
    ep(i, u) {
        if (vis[i ^ 1]) continue; vis[i] = true;
        if (!dfn[v]) {
            ++son, Tarjan(v), low[u] = std::min(low[u], low[v]);
            if (low[v] >= dfn[u] and rt != u) f[u] = true;
        }
        else low[u] = std::min(low[u], dfn[v]);
    }
    if (son > 1 and rt == u) f[u] = true;
}

int main() {
    scanf("%d%d", & n, & m);
    lep(i, 1, m) scanf("%d%d", & u, & v), A(u, v), A(v, u);
    lep(i, 1, n) if (!dfn[i]) rt = i, Tarjan(i);
    
    int tot = 0;
    lep(i, 1, n) if (f[i]) ++tot;
    printf("%d\n", tot);
    lep(i, 1, n) if (f[i]) printf("%d ", i);
    return 0;
}


点双连通分量

点击查看

割点将图分割成了若干个点双,统计即可。


#include <bits/stdc++.h>
#define lep(i, a, b) for (int i = a; i <= b; ++i)
#define rep(i, a, b) for (int i = a; i >= b; --i)
#define ep(i, u) for (int i = H[u], v = e[i].v; i ; i = e[i].n, v = e[i].v)

const int _ = 5e5 + 7 ;
const int __ = 4e6 + 7;

struct edge { int v, n; } e[__]; int cnte = 1, H[_];
int n, m, low[_], dfn[_], idx, u, v, rt, stk[_]; bool vis[__];
std::vector <std::vector<int> > Ans;

void A(int u, int v) { e[++cnte] = { v, H[u] }; H[u] = cnte; }
void Tarjan(int u) { int son = 0;
    low[u] = dfn[u] = ++idx, stk[++*stk] = u;
    ep(i, u) {
        if (vis[i ^ 1]) continue; vis[i] = true;
        if (!dfn[v]) {
            ++son, Tarjan(v), low[u] = std::min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                std::vector <int> x;
                x.push_back(u);
                while (stk[*stk] != v) x.push_back(stk[(*stk)--]);
                x.push_back(stk[(*stk)--]);
                Ans.push_back(x);
            }
        }
        else low[u] = std::min(low[u], dfn[v]);
    }
    if (rt == u and !son) {
        std::vector <int> x;
        x.push_back(u);
        Ans.push_back(x);
    }
}

int main() {
    scanf("%d%d", & n, & m);
    lep(i, 1, m) scanf("%d%d", & u, & v), A(u, v), A(v, u);
    lep(i, 1, n) if (!dfn[i]) rt = i, Tarjan(i);
    
    printf("%d\n", (int)Ans.size());
    for (auto x : Ans) {
        printf("%d ", (int)x.size());
        for (int i : x) printf("%d ", i);
        putchar('\n');
    }
    return 0;
}

例题

[USACO04DEC] Cow Ski Area G

罗恩的雪场可以划分为 WL(1W500,1L500),每个方格有一个特定的高度 H(0H9999)。奶牛可以在相邻方格间滑雪,而且不能由低到高滑。
为了保证任意方格可以互通,罗恩打算造一些直达缆车。缆车很强大,可以连接任意两个方格,而且是双向的。而且同一个方格也可以造多台缆车。但是缆车的建造费用贵得吓人,所以他希望造尽量少的缆车。那最少需要造多少台呢?

点击查看

缩点得到一个 DAG,可以证明使其变为一个强连通图的最小连边数为入度为 0 和出度为 0 的点个数较大值。

证明:
设入度为 0 的点有 n1 个,出度为 0 的点有 n2 个。
每个这样的点所在连通分量想要和其他点连通至少需要一条边,所以 ansmax(n1,n2)
转换为二分图,设其最大匹配为 m,则需要 m 条边就可以将匹配边连接的点变为一个强连通分量,称为中转部分。
剩下的结点两两之间没有边,给他们对应连上,这时候可能会有剩下的结点,和中转部分的点连边,可以发现这样构造 ansmax(n1,n2)
所以, ans=max(n1,n2)


#include <bits/stdc++.h>
#define lep(i, a, b) for (int i = a; i <= b; ++i)
#define rep(i, a, b) for (int i = a; i >= b; --i)

const int _ = 1e6 + 7;

int w, l, a[501][501], stk[_], low[_], dfn[_], idx, bel[_], scc; bool in[_];
std::vector <int> e[_]; int In[_], Out[_];
int di[4] = { 1, -1, 0, 0 };
int dj[4] = { 0, 0, 1, -1 };

int id(int x, int y) { return (x - 1) * w + y; }
void Tarjan(int u) {
    dfn[u] = low[u] = ++idx, stk[++*stk] = u, in[u] = true;
    for (int v : e[u]) {
        if (!dfn[v]) Tarjan(v), low[u] = std::min(low[u], low[v]);
        else if (in[v]) low[u] = std::min(low[u], dfn[v]);
    }
    if (dfn[u] == low[u]) { ++scc;
        while (stk[*stk] != u) {
            int nw = stk[(*stk)--];
            in[nw] = false, bel[nw] = scc;
        }
        int nw = stk[(*stk)--];
        in[nw] = false, bel[nw] = scc;
    }
}

int main() {
    scanf("%d%d", & w, & l);
    lep(i, 1, l) lep(j, 1, w) scanf("%d", a[i] + j);
    
    lep(i, 1, l) lep(j, 1, w) lep(k, 0, 3) {
        int ni = i + di[k], nj = j + dj[k];
        if (ni < 1 or ni > l or nj < 1 or nj > w or a[ni][nj] > a[i][j]) continue;
        e[id(i, j)].push_back(id(ni, nj));
    }
    
    lep(i, 1, l * w) if (!dfn[i]) Tarjan(i);
    lep(u, 1, l * w)
        for (int v : e[u]) if (bel[u] != bel[v]) ++Out[bel[u]], ++In[bel[v]];
    
    if (scc == 1) { puts("0"); return 0; }
    int t1 = 0, t2 = 0;
    lep(i, 1, scc) { if (!In[i]) ++t1; if (!Out[i]) ++t2; }
    printf("%d\n", std::max(t1, t2));
    return 0;
}

[国家集训队] 稳定婚姻

我们已知 n 对夫妻的婚姻状况,称第 i 对夫妻的男方为 Bi,女方为 Gi。若某男 Bi 与某女 Gj 曾经交往过(无论是大学,高中,亦或是幼儿园阶段,ij),则当某方与其配偶(即 BiGiBjGj)感情出现问题时,他们有私奔的可能性。不妨设 Bi 和其配偶 Gi 感情不和,于是 BiGj 旧情复燃,进而 Bj 因被戴绿帽而感到不爽,联系上了他的初恋情人 Gk ……一串串的离婚事件像多米诺骨牌一般接踵而至。若在 BiGi 离婚的前提下,这 2n 个人最终依然能够结合成 n 对情侣,那么我们称婚姻 i 为不安全的,否则婚姻 i 就是安全的。
给定所需信息,你的任务是判断每对婚姻是否安全。

点击查看

容易发现重新组合的过程组成了一个环,建有向图找环。
在同一个强联通分量(不是边双联通分类)里的婚姻是不安全的。


#include <bits/stdc++.h>
#define lep(i, a, b) for (int i = a; i <= b; ++i)
#define rep(i, a, b) for (int i = a; i >= b; --i)
#define ep(i, u) for (int i = H[u], v = e[i].v; i ; i = e[i].n, v = e[i].v)

typedef std::string Str;
const int _ = 1e4 + 7 ;
const int __ = 1e5 + 7;

struct edge { int v, n; } e[__]; int cnte = 1, H[_];
int n, m, low[_], dfn[_], idx, rt, stk[_], scc, bel[_]; bool in[_];
std::map <Str, int> S; Str u[_], v[_]; int tot;

void A(int u, int v) { e[++cnte] = { v, H[u] }; H[u] = cnte; }
void Tarjan(int u) {
    low[u] = dfn[u] = ++idx, stk[++*stk] = u, in[u] = true;
    ep(i, u) {
        if (!dfn[v]) Tarjan(v), low[u] = std::min(low[u], low[v]);
        else if (in[v]) low[u] = std::min(low[u], dfn[v]);
    }
    if (dfn[u] == low[u]) { ++scc;
        while (stk[*stk] != u) { int nw = stk[(*stk)--]; in[nw] = false, bel[nw] = scc; }
        int nw = stk[(*stk)--]; in[nw] = false, bel[nw] = scc;
    }
}

int main() {
    std::ios::sync_with_stdio(false),
    std::cin.tie(nullptr), std::cout.tie(nullptr);
    std::cin >> n;
    lep(i, 1, n) {
        std::cin >> u[i] >> v[i];
        int l = S[u[i]] = ++tot, r = S[v[i]] = ++tot;
        A(r, l);
    }
    std::cin >> m; Str a, b;
    lep(i, 1, m)
        std::cin >> a >> b, A(S[a], S[b]);
    lep(i, 1, tot) if (!dfn[i]) Tarjan(i);
	
    lep(i, 1, n) std::cout << (bel[S[u[i]]] == bel[S[v[i]]] ? "Unsafe\n" : "Safe\n");
    return 0;
}

[POI2006] PRO-Professor Szu

某大学校内有一栋主楼,还有 n 栋住宅楼。这些楼之间由一些单向道路连接,但是任意两栋楼之间可能有多条道路,也可能存在起点和终点为同一栋楼的环路。存在住宅楼无法到达主楼的情况。
现在有一位古怪的教授,他希望每天去主楼上班的路线不同。
一条上班路线中,每栋楼都可以访问任意多次。我们称两条上班路线是不同的,当且仅当两条路线中存在一条路是不同的(两栋楼之间的多条道路被视为是不同的道路)。
现在教授希望知道,从哪些住宅楼前往主楼的上班路线数最多。

点击查看

倒序建图 + 缩点 + Topu


#include <bits/stdc++.h>
#define lep(i, a, b) for (int i = a; i <= b; ++i)
#define rep(i, a, b) for (int i = a; i >= b; --i)

const int _ = 1e6 + 7;
const int inf = 36501;

int n, m, low[_], dfn[_], idx, stk[_], bel[_], scc;
int f[_], In[_], u[_], v[_]; bool vis[_];
std::vector <int> e[_], E[_];

void Tarjan(int u) {
    low[u] = dfn[u] = ++idx, stk[++*stk] = u, In[u] = true;
    for (int v : e[u]) {
        if (!dfn[v]) Tarjan(v), low[u] = std::min(low[u], low[v]);
        else if (In[v]) low[u] = std::min(low[u], dfn[v]);
    }
    if (dfn[u] == low[u]) { ++scc; int tot = 0;
        while (stk[*stk] != u) {
            int nw = stk[(*stk)--]; ++tot, bel[nw] = scc, In[nw] = false;
        }
        int nw = stk[(*stk)--]; ++tot, In[nw] = false, bel[nw] = scc;
        if (tot > 1) vis[scc] = true;
    }
}

int main() {
    scanf("%d%d", & n, & m);
    lep(i, 1, m) scanf("%d%d", v + i, u + i), e[u[i]].push_back(v[i]);
    lep(i, 1, n + 1) if (!dfn[i]) Tarjan(i);
    std::memset(In, 0, sizeof(In));
    
    std::queue <int> d;
    lep(i, 1, m) {
        if (u[i] == v[i]) vis[bel[u[i]]] = true;
        if (bel[u[i]] != bel[v[i]]) E[bel[u[i]]].push_back(bel[v[i]]), ++In[bel[v[i]]];
    }
    
    lep(i, 1, n + 1) if (!In[i]) d.push(i);
    int ans = 0, sum = 0;
    f[bel[n + 1]] = 1;
    while (!d.empty()) {
        int u = d.front(); d.pop();
        if (vis[u] and f[u]) f[u] = inf;
        if (f[u] > ans) ans = f[u];
        
        for (int v : E[u]) {
            f[v] = std::min(inf, f[v] + f[u]), --In[v];
            if (!In[v]) d.push(v);
        }
    }
    
    if (ans == inf) puts("zawsze");
    else printf("%d\n", ans);
    
    std::memset(vis, 0, sizeof(vis));
    lep(i, 1, scc) if (f[i] == ans) vis[i] = true;
    lep(i, 1, n) if (vis[bel[i]]) ++sum;
    printf("%d\n", sum);
    
    lep(i, 1, n) if (vis[bel[i]]) printf("%d ", i);
    return 0;
}

[POI2012] FES-Festival

Byteasar 告诉你,所有参赛者的成绩都是整数秒。他还会为你提供了一些参赛者成绩的关系。具体是:他会给你一些数对 (A,B),表示 A 的成绩正好比 B1 秒;他还会给你一些数对 (C,D),表示 C 的成绩不比 D 慢。而你要回答的是:所有参赛者最多能达到多少种不同的成绩,而不违背他给的条件。

Full_Speed 版题面:

n 个带权点,赋予它们各一个正整数权值,使得满足两类限制。

  • 1 u v : valu=valv1
  • 2 u v : valuvalv
    求最多有多少种不同的点权。
点击查看

用差分约束建图,对于其中的两个点 uv ,可以发现只有当它们强连通时,选择的值域才有上下界差值的限制。
对于一个强连通分量中的点,其贡献为 最长路长度 +1
且多个强连通分量之间不会相互影响,因为它们要么不相连,要么有权为 0 的边,可以通过将值域拉开很远来解决。
所以最终答案就是将各个强连通分量的贡献累加。


#include <bits/stdc++.h>
#define lep(i, a, b) for (int i = a; i <= b; ++i)
#define rep(i, a, b) for (int i = a; i >= b; --i)

typedef long long ll;
const int _ = 600 + 7;
const int __ = 2e5 + 7;
const int inf = 100000;

int n, m1, m2, dis[_][_], low[_], dfn[_], idx, bel[_], stk[_], scc, nw, p[_], ans; bool in[_];

void Tarjan(int u) {
    low[u] = dfn[u] = ++idx, stk[++*stk] = u, in[u] = true;
    lep(v, 1, n) if (dis[u][v] != inf and u != v) {
        if (!dfn[v]) Tarjan(v), low[u] = std::min(low[u], low[v]);
        else if (in[v]) low[u] = std::min(low[u], dfn[v]);
    }
    if (dfn[u] == low[u]) { ++scc;
        while (stk[*stk] != u) { nw = stk[(*stk)--]; bel[nw] = scc, in[nw] = false; }
        nw = stk[(*stk)--]; bel[nw] = scc, in[nw] = false;
    }
}

int main() {
    scanf("%d%d%d", & n, & m1, & m2);
    lep(i, 1, n) { lep(j, 1, n) if (i != j) dis[i][j] = inf; p[i] = i; }
    int u, v;
    lep(i, 1, m1) {
        scanf("%d%d", & u, & v);
        dis[u][v] = std::min(dis[u][v], -1), dis[v][u] = std::min(dis[v][u], 1);
    }
    lep(i, 1, m2) {
        scanf("%d%d", & u, & v);
        dis[u][v] = std::min(dis[u][v], 0);
    }
    
    lep(i, 1, n) if (!dfn[i]) Tarjan(i);
    std::sort(p + 1, p + 1 + n, [](int x, int y) { return bel[x] < bel[y]; });
    int lst = 1, pos = 1;
    while (lst <= n) {
        while (bel[p[pos]] == bel[p[lst]]) ++pos;
        lep(k, lst, pos - 1) lep(i, lst, pos - 1) lep(j, lst, pos - 1) dis[p[i]][p[j]] = std::min(dis[p[i]][p[j]], dis[p[i]][p[k]] + dis[p[k]][p[j]]);
        
        int mx = 0;
        lep(i, lst, pos - 1) {
            if (dis[p[i]][p[i]] < 0) goto Nie;
            lep(j, lst, pos - 1) mx = std::max(dis[p[i]][p[j]] + 1, mx);
        }
        ans += mx,    lst = pos;
    }
    
    printf("%d\n", ans);
    return 0;
    
    Nie:
    puts("NIE");
    return 0;
}

[HNOI2012] 矿场搭建

煤矿工地可以看成是由隧道连接挖煤点组成的无向图。为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处。于是矿主决定在某些挖煤点设立救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。
请写一个程序,用来计算至少需要设置几个救援出口,以及不同最少救援出口的设置方案总数。

点击查看

求点双后分讨,对于一个点双:
如果没有割点,标记其中两个点,当然如果只有一个孤立点就标记一个点。
如果有一个割点,标记除割点外的一个点。
如果有多余(包含)两个割点,则不需标记,因为可以通过没有被堵住的割点去别的点双。


#include <bits/stdc++.h>
#define lep(i, a, b) for (int i = a; i <= b; ++i)
#define rep(i, a, b) for (int i = a; i >= b; --i)
#define ep(i, u) for (int i = H[u], v = e[i].v; i ; i = e[i].n, v = e[i].v)

typedef long long ll;
const int _ = 2e5 + 7;

struct edge { int v, n; } e[_]; int H[_], cnte = 1; bool vis[_];
int n, m, rt, stk[_]; bool f[_];
int dfn[_], low[_], idx;
std::vector <std::vector <int> > SCC;

void A(int u, int v) { e[++cnte] = { v, H[u] }, H[u] = cnte; }
void Tarjan(int u) { int son = 0;
    dfn[u] = low[u] = ++idx, stk[++*stk] = u;
    ep(i, u) {
        if (vis[i ^ 1]) continue; vis[i] = true;
        if (!dfn[v]) {
            Tarjan(v), low[u] = std::min(low[u], low[v]), ++son;
            if (low[v] >= dfn[u]) { int nw;
                if (rt != u) f[u] = true;
                std::vector <int> x;
                x.push_back(u);
                while (stk[*stk] != u)
                    x.push_back(stk[(*stk)--]);
                SCC.push_back(x);
            }
        }
        else low[u] = std::min(low[u], dfn[v]);
    }
    if (rt == u and son > 1) f[u] = true;
}
void C() {
    lep(i, 1, n) f[i] = dfn[i] = low[i] = H[i] = 0; n = idx = *stk = 0;
    lep(i, 1, cnte) vis[i] = false;
    SCC.clear();
    cnte = 1;
}

int main() { int T = 0;
    while (++T) {
        scanf("%d", & m); int u, v;
        if (!m) break;
        lep(i, 1, m) scanf("%d%d", & u, & v), A(u, v), A(v, u), n = std::max(n, std::max(u, v));
        
        lep(u, 1, n) if (!dfn[u]) rt = u, Tarjan(u);
        
        ll ans = 0, tot = 1;
        for (auto x : SCC) { ll len = x.size(), u, tmp = 0;
            for (int i : x)
                if (f[i]) ++tmp, u = i;
            if (tmp == 1) ++ans, tot *= len - 1;
            else if (!tmp) ans += 2, tot *= n * (n - 1) / 2;
        }
        printf("Case %d: %lld %lld\n", T, ans, tot);
        C();
    }
    return 0;
}

[NOIP2022] 建造军营

A 国的国土由 n 座城市组成,m 条双向道路连接这些城市,使得任意两座城市均可通过道路直接或间接到达。A 国打算选择一座或多座城市(至少一座),并在这些城市上各建造一座军营。
众所周知,军营之间的联络是十分重要的。然而此时 A 国接到情报,B 国将会于不久后袭击 A 国的一条道路,但具体的袭击目标却无从得知。如果 B 国袭击成功,这条道路将被切断,可能会造成 A 国某两个军营无法互相到达,这是 A 国极力避免的。因此 A 国决定派兵看守若干条道路(可以是一条或多条,也可以一条也不看守),A 国有信心保证被派兵看守的道路能够抵御 B 国的袭击而不被切断。
A 国希望制定一个建造军营和看守道路的方案,使得 B 国袭击的无论是 A 国的哪条道路,都不会造成某两座军营无法互相到达。现在,请你帮 A 国计算一下可能的建造军营和看守道路的方案数共有多少。由于方案数可能会很多,你只需要输出其对 1,000,000,007(109+7) 取模的值即可。两个方案被认为是不同的,当且仅当存在至少一 座城市在一个方案中建造了军营而在另一个方案中没有,或者存在至少一条道路在一个 方案中被派兵看守而在另一个方案中没有。

点击查看
GE

边三连通分量

对于一张无向图 G=(V,E)

  • 我们称两个点 u,v (u,vV,uv) 是边三连通的,当且仅当存在三条从 u 出发到达 v 的,相互没有公共边的路径。
  • 我们称一个点集 U (UV) 是边三连通分量,当且仅当对于任意两个点 u,v (u,vU,uv) 都是边三连通的。
  • 我们称一个边三连通分量 S 是极大边三连通分量,当且仅当不存在 uSuV,使得 S{u} 也是边三连通分量。
点击查看
GE


圆方树

[COCI2007-2008#1] STAZA

对于任意两个城市之间,保证至少有一条路线,且每条整个交通系统中的每条道路最多是一个环的一部分。
你的任务是找到满足以下两个约束条件的最长路线:

  • 路线可以从任何城市开始,但必须在城市 1 结束。
  • 这条路线可以多次访问同一个城市,但不能经过同一条道路超过一次。
    请你输出最长的路线的长度。
点击查看
GE


作者:qkhm

出处:https://www.cnblogs.com/qkhm/p/18586277/Tarjan

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   qkhm  阅读(50)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
点击右上角即可分享
微信分享提示