前置知识
多源汇最短路算法和 Floyed-Warshall 算法
理论解析
Floyed-Warshall 算法 是解决多源汇最短路问题的核心算法,可以用于求解图中任意两点间的最短路且允许存在负权边(但不允许存在负权回路)。其实质与核心是动态规划转移方程 d i j = m i n ( d i j , d i k + d k j ) dij=min(dij,dik+dkj) dij=min(dij,dik+dkj) 。
传递闭包 集合中二元关系的最小传递关系。通俗地说,就是给定一些二元关系,例如比赛中的输赢关系、家族中的祖先关系、打电话时的通话关系等,然后通过这些给定的关系来判断任意两个关系之间是否连通。
Floyed-Warshall 算法是解决有向图上的传递闭包问题的经典算法,即判断给定有向图上的任意两个点之间是否存在有向路径 (需要深刻理解这句话,"是否存在有向路径")。此时,有向图的边权为 0 或 1。与经典最短路算法不同的是,此时我们的状态转移方程需要改写为:
d i j = d i j ∣ ( d i k & & d k j ) dij = dij | (dik \&\& dkj) dij=dij∣(dik&&dkj)
其中, d i k & & d k j dik\&\&dkj dik&&dkj 就是通过与运算,判断点 i i i 到点 k k k 和点 k k k 到点 j j j 是否均存在可到达的路径。若均为 1,则结果为 1;若任意一条边为 0,则结果为 0。这样,我们就可以判断经过点 k k k 是否存在 i i i 到 j j j 的连通路径。然后或运算可以用于合并状态,将词条路径是否连通合并到 d i j dij dij 中,就可以存储 i i i 到 j j j 的所有连通路径。
关于或运算合并状态及其他二进制的常用技巧,若没有接触过建议移步孤岛营救问题,在本文中笔者对二进制操作的相关技巧进行了简单解释:

由于在传递闭包中状态转移方程被改写为二进制操作,因此我们还可以使用 b i t s e t bitset bitset 来对原代码进行优化,使其复杂度降为 O ( n 3 / l ) O({n^3}/l) O(n3/l),其中 l l l 表示位长。具体实现如下:
cpp
bitset<N> b[N];
for (int k = 1; k <= n; k++)
{
for (int i = 1; i <= n; i++)
{
if (b[i][k]) b[i] |= b[k];
}
}
实际应用
我们通过几道题目来看一下 Floyed-Warshall 算法在传递闭包问题中的具体应用。
例1:【USACO08JAN】 Cow Contest S
题目链接
https://www.luogu.com.cn/problem/P2419
思路分析
本题中其给定的 m m m 组输赢关系实际上就是上文中所提及的二元关系。题目想让我们求出排名可以确定的奶牛的数目,实际上就是要求有多少个奶牛已经和其他 n − 1 n-1 n−1 头奶牛有了稳定确定的二元关系,只要它和其他所有奶牛的输赢关系都已确定,我们就可以确定其排名。
值得一提的是,在统计时,我们可以进行这样一个优化:所谓关系确定,即 d i s i j disij disij 和 d i s j i disji disji 任意一个的值为 1,题目保证不会自相矛盾,所以不可能同时为 1。
对于每一个点 i i i,我们去遍历与其相异的点 j j j,如果 d i s i j ∣ ∣ d i s j i disij ||disji disij∣∣disji 的结果为 1,则关系确定;如果结果为 0,则关系不确定。再预设一个 r e s = 1 res=1 res=1,与其不断做与运算,如果同时为 1,则关系确定, r e s res res 为1;只要到任意一点 j j j 时或运算得 0,则关系不确定, r e s res res 最终为0,即点 i i i 存在与其关系不确定的点。
AC代码
cpp
#include <iostream>
#include <cstdio>
using namespace std;
int n, m, ans;
int dis[110][110];
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
int a, b;
scanf("%d%d", &a, &b);
dis[a][b] = 1;
}
for (int k = 1; k <= n; k++)
{
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
dis[i][j] |= dis[i][k] && dis[k][j];
}
}
}
for (int i = 1; i <= n; i++)
{
int res = 1;
for (int j = 1; j <= n; j++)
{
if (i == j) continue;
res = res && (dis[i][j] || dis[j][i]);
}
ans += res;
}
printf("%d", ans);
return 0;
}
例2:【ECNA 2001】排序
题目链接
https://www.luogu.com.cn/problem/P1347
思路分析
题目要求我们根据前 x x x 个关系来确定其是否顺序已经确定、关系存在矛盾或者关系不定。因此我们每输入一对关系,就要跑一遍 Floyed 算法算出通达情况,然后检查当前关系的状态,根据当前状态来决定是否不再继续往下判断。 d i s i j disij disij 记录 i < j i<j i<j 的情况,转化为是否存在 i i i 到 j j j 的有向路径。
设 0 为关系不确定,1 为关系存在矛盾,2 为关系已经确定。
- 关系不确定(0): 关系不确定,即存在两个点 i i i, j j j, d i s i j disij disij 和 d i s j i disji disji 均为0,不存在大小比较的关系
- 关系存在矛盾(1): 对于两个点 i i i, j j j,如果 d i s i j = 1 disij=1 disij=1 且 d i s j i = 1 disji=1 disji=1,则会形成一个自环,使得 d i s i i = 1 disii=1 disii=1。比如 1 可以到 2,2也可以到 1,那就可以推出 1 可以到 1。因此判断关系存在矛盾,只需要遍历每个点,判断其 d i s i i disii disii 是否等于 1 即可。
- 关系已经确定(2): 对于任意两个点 i i i 和 j j j,只要都存在 d i s i j = 1 disij=1 disij=1或 d i s j i = 1 disji=1 disji=1(二者能且仅能满足成立其一),且不存在 d i s i i = 1 disii=1 disii=1,则关系可以确定。在实际代码实现中,只要前两种情况均排除则可以判断为第三种情况。
AC代码
cpp
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
int n, m;
int st[30];
int dis[30][30], G[30][30];
void floyed()
{
memcpy(dis, G, sizeof dis);
for (int k = 1; k <= n; k++)
{
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
dis[i][j] |= dis[i][k] && dis[k][j];
}
}
}
return;
}
int check()
{
//有矛盾
for (int i = 1; i <= n; i++)
{
if (dis[i][i]) return 1;
}
//不确定
for (int i = 1; i <= n; i++)
{
for (int j = 1; j < i; j++)
{
if (!dis[i][j] && !dis[j][i]) return 0;
}
}
return 2;
}
char get_min()
{
for (int i = 1; i <= n; i++)
{
if (!st[i])
{
bool flag = true;
for (int j = 1; j <= n; j++)
{
if (!st[j] && dis[j][i])
{
flag = false;
break;
}
}
if (flag)
{
st[i] = true;
return 'A' + i - 1;
}
}
}
}
int main()
{
while (cin >> n >> m, n || m)
{
memset(st, 0, sizeof st);
memset(G, 0, sizeof G);
int type = 0, t;
for (int i = 1; i <= m; i++)
{
string s;
cin >> s;
int a = s[0] - 'A' + 1, b = s[2] - 'A' + 1;
if (!type)
{
G[a][b] = 1;
floyed();
type = check();
if (type) t = i;
}
}
if (!type) printf("Sorted sequence cannot be determined.\n");
else if (type == 1) printf("Inconsistency found after %d relations.\n", t);
else
{
printf("Sorted sequence determined after %d relations: ", t);
for (int i = 1; i <= n; i++) printf("%c", get_min());
printf(".\n");
}
}
return 0;
}
其他推荐练习
Uva247 Calling Circles(洛谷链接:https://www.luogu.com.cn/problem/UVA247)