【ACWing】1175. 最大半连通子图

题目地址:

https://www.acwing.com/problem/content/1177/

一个有向图 G = ( V , E ) G=(V,E) G=(V,E)称为半连通的(Semi-Connected),如果满足: ∀ u , v ∈ V ∀u,v∈V u,vV,满足 u → v u→v uv v → u v→u vu,即对于图中任意两点 u , v u,v u,v,存在一条 u u u v v v的有向路径或者从 v v v u u u的有向路径。若 G ′ = ( V ′ , E ′ ) G'=(V',E') G=(V,E)满足, E ′ E' E E E E中所有和 V ′ V' V有关的边,则称 G ′ G′ G G G G的一个导出子图。若 G ′ G′ G G G G的导出子图,且 G ′ G' G半连通,则称 G ′ G' G G G G的半连通子图。若 G ′ G' G G G G所有半连通子图中包含节点数最多的,则称 G ′ G' G G G G的最大半连通子图。给定一个有向图 G G G,请求出 G G G的最大半连通子图拥有的节点数 K K K,以及不同的最大半连通子图的数目 C C C。由于 C C C可能比较大,仅要求输出 C C C X X X的余数。

输入格式:
第一行包含三个整数 N , M , X N,M,X N,M,X N , M N,M N,M分别表示图 G G G的点数与边数, X X X的意义如上文所述;接下来 M M M行,每行两个正整数 a , b a,b a,b,表示一条有向边 ( a , b ) (a,b) (a,b)。图中的每个点将编号为 1 1 1 N N N,保证输入中同一个 ( a , b ) (a,b) (a,b)不会出现两次。

输出格式:
应包含两行。第一行包含一个整数 K K K,第二行包含整数 C m o d    X C \mod X CmodX

数据范围:
1 ≤ N ≤ 1 0 5 1≤N≤10^5 1N105
1 ≤ M ≤ 1 0 6 1≤M≤10^6 1M106
1 ≤ X ≤ 1 0 8 1≤X≤10^8 1X108

注意这里的半连通子图的定义,这里的定义是取一些点,和与这些点关联的所有边。所以不同的半连通子图只会在点上有区别,而不会在边上有区别。对于拓扑图来说,最大的半连通子图实际上就是某个从入度为 0 0 0的点出发的最长链(最长链一定走到了某个出度为 0 0 0的点)。所以容易想到先求强连通分量,然后缩点化为拓扑图来做。化为拓扑图之后,按拓扑序递推即可。这里还需要注意,在缩点之后的新图里,是不能有平行边的(严格来说可以有,但是在递推的时候需要注意判重,所以索性就每条边建一次就好),这样递推的时候就不会因为平行边而累加多次。设 f [ i ] f[i] f[i]是新图里走到 i i i的最长链的点总数(这里的“最长链”并不代表边数最多,而是代表总点数最多,因为缩点之后每个点实际上是由多个点捏成一个得到的),那么 f [ j ] = s [ j ] + max ⁡ i → j f [ i ] f[j]=s[j]+\max_{i\to j} f[i] f[j]=s[j]+ijmaxf[i]即以点 j j j结尾的最长链应当是其前驱为节点的最长链加上 j j j自己的点数。代码如下:

#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;

const int N = 1e5 + 10, M = 2e6 + 10;
int n, m, mod;
int h[N], hs[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt, sz[N];
// f[i]存的是以i结尾的最长链总点数,g[i]存的是以i结尾的最长链的条数
int f[N], g[N];

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 = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (!dfn[v]) {
            tarjan(v);
            low[u] = min(low[u], low[v]);
        } else if (in_stk[v]) low[u] = min(low[u], dfn[v]);
    }

    if (dfn[u] == low[u]) {
        scc_cnt++;
        int y;
        do {
            y = stk[--top];
            in_stk[y] = false;
            id[y] = scc_cnt;
            sz[scc_cnt]++;
        } while (y != u); 
    }
}

int main() {
    memset(h, -1, sizeof h);
    memset(hs, -1, sizeof hs);

    scanf("%d%d%d", &n, &m, &mod);
    while (m--) {
        int a, b;
        scanf("%d%d", &a, &b);
        add(h, a, b);
    }
    
    for (int i = 1; i <= n; i++)
        if (!dfn[i])
            tarjan(i);

	// 建新图。开一个哈希表对边判重
    unordered_set<long> S;
    for (int i = 1; i <= n; i++)
        for (int j = h[i]; ~j; j = ne[j]) {
            int k = e[j];
            int a = id[i], b = id[k];
            long hash = a * 1e6 + b;
            if (a != b && !S.count(hash)) {
                add(hs, a, b);
                S.insert(hash);
            }
        }

	// tarjan算法结束后,所有的强连通分量实际上是按照编号逆序成拓扑序的,
	// 所以逆序遍历所有的强连通分量就可以做递推了。
    for (int i = scc_cnt; i; i--) {
        if (!f[i]) {
            f[i] = sz[i];
            g[i] = 1;
        }

        for (int j = hs[i]; ~j; j = ne[j]) {
            int k = e[j];
            if (f[k] < f[i] + sz[k]) {
                f[k] = f[i] + sz[k];
                g[k] = g[i];
            } else if (f[k] == f[i] + sz[k]) g[k] = (g[k] + g[i]) % mod;
        }
    }

    int maxf = 0, 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;
}

时空复杂度 O ( V + E ) O(V+E) O(V+E)

堆排序是一种高效的排序算法,它利用了堆的数据结构来实现排序。堆是一个完全二叉树,具有以下性质:对于任意节点 i,其父节点的值小于等于子节点的值。 堆排序的基本思路如下: 1. 构建最大堆:将待排序的数组看作是一个完全二叉树,从最后一个非叶子节点开始,依次向上调整每个节点,使得以该节点为根的子树满足最大堆的性质。 2. 将堆顶元素与最后一个元素交换:将最大堆的堆顶元素(即数组的第一个元素)与数组最后一个元素交换位置,此时最大元素就位于数组的最后。 3. 调整堆:将剩余元素重新调整为最大堆。 4. 重复步骤 2 和步骤 3,直到所有元素都排序完成。 以下是堆排序的 C++ 代码实现: ```cpp #include <iostream> using namespace std; // 调整以 root 为根的子树为最大堆 void heapify(int arr[], int n, int root) { int largest = root; // 假设根节点最大 int left = 2 * root + 1; // 左子节点索引 int right = 2 * root + 2; // 右子节点索引 // 若左子节点大于根节点,更新最大值索引 if (left < n && arr[left] > arr[largest]) { largest = left; } // 若右子节点大于最大值节点,更新最大值索引 if (right < n && arr[right] > arr[largest]) { largest = right; } // 若最大值不是根节点,交换根节点和最大值 if (largest != root) { swap(arr[root], arr[largest]); // 递归调整交换后的子树 heapify(arr, n, largest); } } void heapSort(int arr[], int n) { // 构建最大堆 for (int i = n / 2 - 1; i >= 0; i--) { heapify(arr, n, i); } // 逐步取出最大值,调整堆 for (int i = n - 1; i > 0; i--) { swap(arr[0], arr[i]); heapify(arr, i, 0); } } int main() { int arr[] = {4, 10, 3, 5, 1}; int n = sizeof(arr) / sizeof(arr[0]); heapSort(arr, n); cout << "Sorted array: "; for (int i = 0; i < n; i++) { cout << arr[i] << " "; } cout << endl; return 0; } ``` 以上就是堆排序的基本思路和实现方法。堆排序的时间复杂度为 O(nlogn),其中 n 为数组的长度。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值