【OI学习笔记】Floyed-Warshall算法解决传递闭包问题

前置知识

多源汇最短路算法和 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

相关推荐
国科安芯1 小时前
卫星星务计算机数据管理中SEU容错机制的技术实现研究——以AS32S601存储器ECC架构为例
网络·单片机·算法·fpga开发·架构·云计算·risc-v
孙启超1 小时前
【AI应用开发】Agent 无限 loop(反复调用同一个工具)如何规避?
人工智能·python·算法·llm·agent·loop·ai应用开发
鱼听禅1 小时前
C#学习笔记-添加编译参数到程序集自动更新程序编译时间
笔记·学习·c#
zmzb01031 小时前
C++课后习题训练记录Day179
开发语言·c++
小月土星1 小时前
LeetCode 142. 环形链表 II 超详细题解(快慢指针原理+逐行代码拆解)
算法
香菜TTT1 小时前
LangChain框架_学习笔记
笔记·学习·langchain
春水碧于天,画船听雨眠2 小时前
LangChain学习笔记(二)
笔记·学习·langchain
dok122 小时前
Johannes 《Linux内核模块与设备驱动开发:编写Linux驱动程序》 (11)kmalloc 动态内存分配和struct file中的私有数据
linux·驱动开发·算法
hansang_IR2 小时前
【题解】[COCI 2024/2025 #2] 流明 / Blistavost
c++·算法·动态规划·dp