拓扑排序
基础
拓扑排序,一般指在一个 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
X−1 也一定合法;反之则
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 u→v,则经过这条边的最短路条数就是:以 u u u 为终点的最短路条数 × \times × 以 v v v 为起点的最短路条数。
先判断被最短路经过的边有哪些。Dijkstra
或 SPFA
的松弛操作长这样:
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(u→v))→disv
显然在这两种算法结束后,对于一条边
u
→
v
u\to v
u→v,如果
d
i
s
v
=
d
i
s
u
+
w
(
u
→
v
)
dis_v=dis_u+w(u\to v)
disv=disu+w(u→v),则说明
v
v
v 的最短路经过了
u
→
v
u\to v
u→v 这条边(或者边权与之相等的边),即
u
→
v
u\to v
u→v 被至少一条最短路包含。
如果我们钦定一个源点
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+fv→fv 即可。 g g g 可以按照计算 f f f 时的拓扑序反着统计。初始时 g i = 1 g_i=1 gi=1,转移时对于一条边 u → v u\to v u→v, g u + g v → g u g_u+g_v\to g_u gu+gv→gu 即可。
最终对于一条边 u → v u\to v u→v,其答案即为 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;
}