拓扑排序 算法解析+例题

拓扑排序


基础

拓扑排序,一般指在一个 DAG (有向无环图)中将顶点排序,使得对于图上任意一条有向边 ( u , v ) (u,v) (u,v) u u u 在拓扑排序后在 v v v 的前面。这个排序的结果称为拓扑序。显然拓扑序一般不唯一。

因为拓扑序满足后面的点不会对前面的点产生依赖,所以经常和动态规划结合在一起使用,保证后面的点不会影响前面已经计算过贡献的点。一张图存在拓扑序,当且仅当这是张 DAG,所以一个常见的套路就是:先对原图进行缩点,然后用拓扑+动态规划等统计答案。

拓扑算法基本流程:记录图中每个点的入度 d d d,将入度等于 0 0 0 的点入队。每次取出一个队首 u u u,将每一个与 u u u 相连的点 v v v 的入度减一(相当于从图中删除 u u u 和与其相连的出边)。如果 d v = 0 d_v=0 dv=0,则 v v v 也入队。一直重复到队列为空。时间复杂度 O ( n + m ) O(n+m) O(n+m),其中 n n n 为点数, m m m 为边数。

vector<int> mp[maxn];
int d[maxn],n,ans[maxn],tot;
bool toposort() {
    queue<int> q;
    for (int i = 1;i <= n;i ++)
        if (d[i] == 0) q.push(i);
    int u;
    while (!q.empty()) {
        u = ans[++ tot] = q.front(); q.pop();
        for (auto v : mp[u])
            if (--d[v] == 0) q.push(v);
    }
    return tot == n; // 判断是否有环,有则返回 True
}

一个最典型的应用就是对一个 AOV 网判断是否有若干个活动组成了环。AOV 网就是一张有向图,顶点表示活动,边表示活动之间进行的顺序。如果原图存在环,则最后的拓扑序不会包含所有的结点。利用这个性质就可以用来判环。

另一个应用就是计算一个 AOE 网的关键路径的长度。AOE 网与 AOV 网类似,只是边有了边权(时间),且一条边代表一个活动,而点代表一个事件;关键路径是指从起点到终点的最长路径。我们可以在拓扑的过程中用动态规划求解。记 f u f_u fu 表示触法 u u u 这个事件的最晚时间。假设当前从队列中取出 u u u,与其相连的是点 v v v,边权为 w w w,则 max ⁡ ( f u + w , f v ) → f v \max(f_u+w,f_v)\to f_v max(fu+w,fv)fv

例题

简单题(黄题 → \to 绿题)

前者在拓扑的过程中 dp 统计,后者与上文中提到的 AOE 网络类似。当然也可以直接跑最长路:)

也是和 dp 结合统计答案,只不过需要套一个分数计算。

中等题(蓝题)

题目中提到“符合前 X X X 个观察结果”,这提示我们二分这个 X X X,如果合法则 X − 1 X-1 X1 也一定合法;反之则 X + 1 X+1 X+1 也一定不合法,显然 X X X 具有单调性。check 的时候拓扑判环即可。

因为要计算字典序最小的拓扑序,所以把原算法中的队列改成小根堆即可。时间复杂度 O ( ( m + n ) log ⁡ ( m + n ) ) O((m+n)\log (m+n)) O((m+n)log(m+n))

题解

难题(紫题)

对于一条边 u → v u\to v uv,则经过这条边的最短路条数就是:以 u u u 为终点的最短路条数 × \times × v v v 为起点的最短路条数。

先判断被最短路经过的边有哪些。DijkstraSPFA 的松弛操作长这样:
min ⁡ ( d i s v , d i s u + w ( u → v ) ) → d i s v \min(dis_v,dis_u+w(u\to v))\to dis_v min(disv,disu+w(uv))disv
显然在这两种算法结束后,对于一条边 u → v u\to v uv,如果 d i s v = d i s u + w ( u → v ) dis_v=dis_u+w(u\to v) disv=disu+w(uv),则说明 v v v 的最短路经过了 u → v u\to v uv 这条边(或者边权与之相等的边),即 u → v u\to v uv​ 被至少一条最短路包含。

如果我们钦定一个源点 S S S,跑最短路后把这几条被最短路经过的边单独拎出来建个图,可以发现这是张 DAG(显然最短路不会走回头路)。

还有一个性质:对于一条最短路 P P P,其路径上任意两点之间的最短路,就是 P P P​ 上以这两点为起点和终点的路径。

然后就可以在新建的由最短路径组成的图上拓扑排序+动态规划了。设 f i , g i f_i,g_i fi,gi 分别表示以 i i i 为终点、起点的最短路条数。显然 f S = 1 f_S=1 fS=1。转移过程中,设从 u u u v v v 转移, f u + f v → f v f_u+f_v\to f_v fu+fvfv 即可。 g g g 可以按照计算 f f f 时的拓扑序反着统计。初始时 g i = 1 g_i=1 gi=1,转移时对于一条边 u → v u\to v uv g u + g v → g u g_u+g_v\to g_u gu+gvgu 即可。

最终对于一条边 u → v u\to v uv,其答案即为 f u × g v f_u \times g_v fu×gv

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1505,maxm = 5005,P = 1e9 + 7;
int n,m,cnt,head[maxn],dis[maxn],d[maxn];
bool vis[maxn],is[maxm]; int ans[maxm];
int ans1[maxn],ans2[maxn],topq[maxn],tot;
struct edge {
    int nxt,w,u,v;
} e[maxm];
void addEdge(int u,int v,int w) {
    e[++ cnt] = edge{head[u],w,u,v};
    head[u] = cnt;
}
void spfa(int st) {
    memset(dis,0x3f,sizeof(dis));
    memset(is,0,sizeof(is));
    queue<int> q; q.push(st); dis[st] = 0;
    while (!q.empty()) {
        int u = q.front(); q.pop(); vis[u] = false;
        for (int i = head[u];i;i = e[i].nxt) {
            int v = e[i].v, w = e[i].w;
            if (dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w;
                if (!vis[v]) { 
                    vis[v] = true;
                    q.push(v); 
                }
            }
        }
    }
    for (int i = 1;i <= m;i ++) {
        int u = e[i].u, v = e[i].v, w = e[i].w;
        if (dis[v] == dis[u] + w) is[i] = true;
        // cout << is[i] << ' ';
    }
    // putchar('\n');
}
void topo(int st) {
    memset(d,0,sizeof(d));
    memset(ans1,0,sizeof(ans1));
    memset(ans2,0,sizeof(ans2));
    queue<int> q; ans1[st] = 1; tot = 0;
    for (int i = 1;i <= m;i ++)
        if (is[i]) d[e[i].v] ++;
    q.push(st); // st为起点,入度一定为0,其他点入读一定不为0(不是起点)
    while (!q.empty()) {
        int u = q.front(); q.pop(); topq[++ tot] = u;
        for (int i = head[u];i;i = e[i].nxt) {
            int v = e[i].v;
            if (!is[i]) continue;
            ans1[v] = (ans1[v] + ans1[u]) % P;
            if (--d[v] == 0) q.push(v);
        }
    }
    for (int j = tot;j > 0;j --) {
        int u = topq[j]; ans2[u] ++;
        for (int i = head[u];i;i = e[i].nxt) {
            int v = e[i].v;
            if (!is[i]) continue;
            ans2[u] = (ans2[u] + ans2[v]) % P;
        }
    }
}
int main() {
    scanf("%d%d",&n,&m);
    for (int i = 1,u,v,w;i <= m;i ++) {
        scanf("%d%d%d",&u,&v,&w);
        addEdge(u,v,w);
    }
    for (int st = 1;st <= n;st ++) {
        spfa(st); topo(st);
        for (int i = 1;i <= m;i ++) {
            if (is[i])
                ans[i] = (ans[i] + (1ll * ans1[e[i].u] * ans2[e[i].v]) % P) % P;
        }
    }
    for (int i = 1;i <= m;i ++)
        printf("%d\n",ans[i]);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值