标签: 图论

Floyd-Warshall算法原理及实现

Floyd-Warshall算法原理及实现

概述

Floyd-Warshall算法,或者简称为Floyd算法,是一种方便好写的全图最短路算法,适用于求全图任意点对最短路的情况下,复杂度较高。下面介绍该算法的原理及实现细节。

算法原理

流程

Floyd算法是一种运用动态规划思想求最短路的经典算法,因此读者需要以动态规划的思想来理解该算法的流程。
对于给定的一个图,我们需要得到它的任意点对最短路结果,以数组$dis(u, v)$来表示点$u$到点$v$的最短路距离。
为了使问题简化,我们规定,给定的图中无自环与重边。
最初,我们需要给这个数组设置初始值。对于图中的任意点$u$,$dis(u, u) = 0$。对于图中的任意有向边$(u, v, w)$,$dis(u, v) = w$。其他的点对值设置为$\infty$。
随后,我们需要枚举“跳板”点$k$,寻找是否存在一条由$(u, k)$与$(k, v)$两条路径拼成的比当前找到的最短路更短。在这里,我们用三重循环,首先枚举$k$,再枚举点对$(u, v)$,并用以下方程对$(u, v)$进行松弛操作。
$$ dis(u, v) = \min_{k \in G} \{dis(u, k) + dis(k, v)\} $$
完成以上步骤后,$dis$数组中的结果即是我们需要的全图最短路了。

复杂度分析

算法流程中,三重循环的位置是复杂度最高的一部分,由于枚举$k$进行$|V|$次,而枚举点对$(u, v)$进行$|V|^2$次,该算法的复杂度为$O(|V|^3)$。注:$V$表示图$G$中的点集。

算法的扩展应用

判断负环

如果我们令$dis$数组的初值全设为0,进行Floyd算法后,如果存在点$u$满足$dis(u, u) < 0$,则说明$u$点在一个负环中。

存在重边、自环时的处理方法

自环如为正边可不必加入该边,如为负边则存在负环。
重边只保留权值最小边即可。
实现中,通用的处理方法为设置$dis$数组为点对间的最短边即可。

实现

下面是一种Floyd算法在C++语言中的实现方法。
输入格式、输出格式、数据范围等参见代码。

// Code by KSkun, 2019/1
#include <cstdio>
#include <cctype>
#include <cstring>

#include <algorithm>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
        ? EOF : *p1++;
}

inline LL readint() {
    LL res = 0, neg = 1; char c = fgc();
    for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
    for(; isdigit(c); c = fgc()) res = res * 10 + c - '0';
    return res * neg;
}

inline char readsingle() {
    char c;
    while(!isgraph(c = fgc())) {}
    return c;
}

const int MAXN = 105, INF = 0x3f3f3f3f;

int n, m;
int dis[MAXN][MAXN];

inline void addedge(int u, int v, int w) {
    dis[u][v] = std::min(dis[u][v], w);
}

inline void init() {
    memset(dis, 0x3f, sizeof(dis));
    for(int i = 1; i <= n; i++) dis[i][i] = 0;
}

inline void floyd() {
    for(int k = 1; k <= n; k++) {
        for(int i = 1; i <= n; i++) {
            for(int j = 1; j <= n; j++) {
                dis[i][j] = std::min(dis[i][j], dis[i][k] + dis[k][j]);
            }
        }
    }
}

int main() {
    n = readint(); m = readint();
    init();
    for(int i = 1, u, v, w; i <= m; i++) {
        u = readint(); v = readint(); w = readint();
        addedge(u, v, w);
    }
    floyd();
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            printf("%d ", dis[i][j] >= INF ? -1 : dis[i][j]);
        }
        putchar('\n');
    }
    return 0;
}

其他分析

关于循环顺序

为什么要先枚举$k$,而后枚举点对$(u, v)$,这是一个困扰很多初学者的问题。
我们考虑调换循环顺序,先枚举$u$,而后枚举$k$和$v$,你会发现,如果以这样的顺序松弛,存在$(u, k)$或$(k, v)$在松弛了$(u, v)$之后再被松弛为一个更小的值的情况。
例如,这里有一个例子:
600px Floyd Warshall example.svg - Floyd-Warshall算法原理及实现
(图片来自Wikipedia,使用CC0协议授权使用)
容易发现,在松弛$(1, 2)$并以$3$为“跳板”时,而如果调换了循环顺序,在松弛该点时,$(3, 2)$还未松弛,所以松弛操作失败,容易发现,点$3$在$(1, 2)$的最短路径上,所以上面的松弛失败是异常的。有时,这种失败可能不会影响到答案,但当路径较长的时候,结果错误会影响答案。
因此,读者应当牢记循环的顺序,以避免使用算法时发生这类错误。

后记

退役了还让我写OI技术类文章,我看你是在难为我胖虎QAQ。
不过突然被人问到了Floyd算法的循环顺序问题,才想起来OI生涯中并没好好思考过这个问题。找了一些资料和示例算是想明白了,也觉得自己这样半吊子学算法不太行,于是把这部分内容补到了这篇文章之中。也算是一种对自己的提醒和不忘初心的追求吧。
一个半吊子OIer能混到今天这个程度,该算是我的幸运还是悲哀呢?

参考资料

[NOI2017]游戏 题解

[NOI2017]游戏 题解

题目地址:洛谷:【P3825】[NOI2017]游戏 – 洛谷、BZOJ:Problem 4945. — [Noi2017]游戏

题目描述

狂野飙车是小 L 最喜欢的游戏。与其他业余玩家不同的是,小 L 在玩游戏之余,还精于研究游戏的设计,因此他有着与众不同的游戏策略。
小 L 计划进行 n 场游戏,每场游戏使用一张地图,小 L 会选择一辆车在该地图上完成游戏。
小 L 的赛车有三辆,分别用大写字母A、B、C表示。地图一共有四种,分别用小写字母x、a、b、c表示。其中,赛车A不适合在地图a上使用,赛车B不适合在地图b上使用,赛车C不适合在地图c上使用,而地图x则适合所有赛车参加。适合所有赛车参加的地图并不多见,最多只会有d张。
n 场游戏的地图可以用一个小写字母组成的字符串描述。例如:S=xaabxcbc表示小 L 计划进行 8 场游戏,其中第 1 场和第 5 场的地图类型是x,适合所有赛车,第 2 场和第 3 场的地图是a,不适合赛车A,第 4 场和第 7 场的地图是b,不适合赛车B,第 6 场和第 8 场的地图是c,不适合赛车C。
小 L 对游戏有一些特殊的要求,这些要求可以用四元组 (i,hi,j,hj) 来描述,表示若在第 i 场使用型号为 hi 的车子,则第 j 场游戏要使用型号为 hj 的车子。
你能帮小 L 选择每场游戏使用的赛车吗?如果有多种方案,输出任意一种方案。如果无解,输出 “-1’’(不含双引号)。

题意简述

你正在玩一个赛车游戏,一共有3种车型,每一场有不适合的车型,有的比赛没有不适合的车型,这种比赛的数量$\leq 8$。有一些限制条件,表示某场比赛如果使用某种车型,那么另一场比赛必须使用另种车型。求是否存在每场比赛采用的车型的安排符合上述所有条件。

输入输出格式

输入格式:
输入第一行包含两个非负整数 n,d 。
输入第二行为一个字符串 S 。 n,d,S 的含义见题目描述,其中 S 包含 n 个字符,且其中恰好 d 个为小写字母 x 。
输入第三行为一个正整数 m ,表示有 m 条用车规则。接下来 m 行,每行包含一个四元组 i,hi,j,hj ,其中 i,j 为整数, hi,hj 为字符a、b或c,含义见题目描述。

输出格式:
输出一行。
若无解输出 “-1’’(不含双引号)。
若有解,则包含一个长度为 n 的仅包含大写字母A、B、C的字符串,表示小 L 在这 n 场游戏中如何安排赛车的使用。如果存在多组解,输出其中任意一组即可。

输入输出样例

输入样例#1:

3 1
xcc
1
1 A 2 B

输出样例#1:

ABA

说明

game noi17 - [NOI2017]游戏 题解

题解

本题可以使用2-SAT解决。
如果不存在可以使用三种的游戏,那么我们按如下方式建图。

  1. 如果$h_i$是$i$不能用的车型,则不作任何操作;
  2. 如果不满足1,且$h_j$是$j$不能用的车型,则标注$(i, h_i)$为“不可能选择”的方案,即连接它与$i$的另外一个选项;
  3. 如果不满足1和2,则有两种选择,即选$(i, h_i)$必须选$(j, h_j)$,选$j$的另外一个选项必须选$i$的另外一个选项,如此建边即可。

跑完2-SAT输出方案就好。对于可以使用三种车型的游戏,则枚举它不能使用某种车型,就转变为类似普通游戏的模型,枚举到一个方案判断一次即可。

代码

// Code by KSkun, 2018/7
#include <cstdio>
#include <cctype>
#include <cstring>

#include <algorithm>
#include <vector>
#include <stack>
#include <queue>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
        ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1; register char c = fgc();
    for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
    for(; isdigit(c); c = fgc()) res = (res << 1) + (res << 3) + c - '0';
    return res * neg;
}

inline char readsingle() {
    char c;
    while(!isgraph(c = fgc())) {}
    return c;
}

const int MAXN = 100005;

int n, m;
char s[MAXN], s1[MAXN];

struct Info {
    int i, j;
    char hi, hj;
} infos[MAXN];

struct Edge {
    int to, nxt;
} gra[MAXN << 1], gran[MAXN << 1];
int head[MAXN], headn[MAXN], tot, totn;

inline void addedge(int u, int v) {
    gra[tot] = Edge {v, head[u]}; head[u] = tot++;
}

inline void addedgen(int u, int v) {
    gran[totn] = Edge {v, headn[u]}; headn[u] = totn++;
}

inline void reset() {
    tot = totn = 0;
    memset(head, -1, sizeof(head));
    memset(headn, -1, sizeof(headn));
}

int dfn[MAXN], low[MAXN], clk, sno[MAXN], scc, op[MAXN];
std::stack<int> sta;
bool insta[MAXN];

void tarjan(int u) {
    dfn[u] = low[u] = ++clk;
    sta.push(u); insta[u] = true;
    for(int i = head[u]; ~i; i = gra[i].nxt) {
        int v = gra[i].to;
        if(!dfn[v]) {
            tarjan(v);
            low[u] = std::min(low[u], low[v]);
        } else if(insta[v]) {
            low[u] = std::min(low[u], dfn[v]);
        }
    }
    if(low[u] == dfn[u]) {
        scc++;
        int p;
        do {
            p = sta.top(); sta.pop();
            insta[p] = false;
            sno[p] = scc;
        } while(p != u);
    }
}

int deg[MAXN];
std::queue<int> que;
int mark[MAXN];

void dfs_mark(int u) {
    if(mark[u]) return;
    mark[u] = -1;
    for(int i = headn[u]; ~i; i = gran[i].nxt) {
        int v = gran[i].to;
        dfs_mark(v);
    }
}

inline void toposort() { 
    for(int i = 1; i <= scc; i++) {
        if(!deg[i]) que.push(i);
    }
    while(!que.empty()) {
        int u = que.front(); que.pop();
        if(mark[u]) continue;
        mark[u] = 1;
        dfs_mark(op[u]);
        for(int i = headn[u]; ~i; i = gran[i].nxt) {
            int v = gran[i].to;
            if(!--deg[v]) que.push(v);
        }
    }
}

inline int id(int x, char y) {
    if(s1[x] == 'a') {
        if(y == 'B') return x;
        else return x + n;
    } else {
        if(y == 'A') return x;
        else return x + n;
    }
}

inline char choice(int x) {
    int xx = x > n ? x - n : x;
    if(s1[xx] != 'a' && id(xx, 'A') == x) return 'A';
    else if(s1[xx] != 'b' && id(xx, 'B') == x) return 'B';
    else if(s1[xx] != 'c' && id(xx, 'C') == x) return 'C';
}

inline int oppo(int x) {
    if(x > n) return x - n;
    else return x + n;
}

inline bool work() {
    reset();
    for(int i = 1; i <= m; i++) {
        Info x = infos[i];
        if(s1[x.i] == tolower(x.hi)) {
            continue;
        } else if(s1[x.j] == tolower(x.hj)) {
            addedge(id(x.i, x.hi), oppo(id(x.i, x.hi)));
        } else {
            addedge(id(x.i, x.hi), id(x.j, x.hj));
            addedge(oppo(id(x.j, x.hj)), oppo(id(x.i, x.hi)));
        }
    }
    memset(dfn, 0, sizeof(dfn));
    memset(low, 0, sizeof(low));
    for(int i = 1; i <= n * 2; i++) {
        if(!dfn[i]) tarjan(i);
    }
    for(int i = 1; i <= n; i++) {
        if(sno[i] == sno[i + n]) return false;
        op[sno[i]] = sno[i + n];
        op[sno[i + n]] = sno[i];
    }
    memset(deg, 0, sizeof(deg));
    for(int u = 1; u <= n * 2; u++) {
        for(int i = head[u]; ~i; i = gra[i].nxt) {
            int v = gra[i].to;
            if(sno[u] != sno[v]) {
                addedgen(sno[v], sno[u]);
                deg[sno[u]]++;
            }
        }
    }
    toposort();
    for(int i = 1; i <= n; i++) {
        if(mark[sno[i]] == 1) putchar(choice(i));
        else putchar(choice(i + n));
    }
    return true;
}

bool dfs_s(int step) {
    if(step > n) return work();
    if(s[step] != 'x') {
        s1[step] = s[step];
        if(dfs_s(step + 1)) return true;
    } else {
        s1[step] = 'a';
        if(dfs_s(step + 1)) return true;
        s1[step] = 'b';
        if(dfs_s(step + 1)) return true;
        s1[step] = 'c';
        if(dfs_s(step + 1)) return true;
    }
}

int main() {
    n = readint(); readint();
    for(int i = 1; i <= n; i++) {
        s[i] = readsingle();
    }
    m = readint();
    for(int i = 1; i <= m; i++) {
        infos[i].i = readint();
        infos[i].hi = readsingle();
        infos[i].j = readint();
        infos[i].hj = readsingle();
    }
    if(!dfs_s(1)) puts("-1");
    return 0;
}
[SNOI2013]Quare 题解

[SNOI2013]Quare 题解

题目地址:BZOJ:Problem 3590. — [Snoi2013]Quare

题目描述

4.20四川芦山地震发生后,抗震救灾委员会接到一个紧急任务,四川省给该委员会发了一份地图,这份地图给出了该省一些城市的情况:任两个城市是用一条或多条公路连接起来的,也可以没有公路连接,但是每个城市都可以直接或间接地到达另外的城市,注意这些公路是可以双向行驶的。由于最近余震、暴雨造成泥石流倾泻,使得车辆在这些公路上行驶很不安全,于是四川省决定尽快对部分公路进行抢修,以保障救援车辆行车安全。
该省对所有的公路情况都进行了勘察,分析估计了抢修某段公路所需要花费的时间,并记录在地图中。现在该省希望抗震救灾委员会能找到一个方案,该方案决定出哪些公路需要抢修,使得抢修后的公路仍能保证任意两个城市之间都能直接或间接地相连,同时为了安全起见,即使某一条抢修的公路被泥石流阻断了,任意两城市仍能保持这个性质。由于时间紧迫,抗震救灾委员会还需保证找到的这个方案总抢修时间最短。

题意简述

给你一个连通无向图,求这个图的一个包含所有点的最小权边双连通子图。

输入输出格式

输入格式:
输入文件有多组数据,第1行为 1 个整数t,为 case总数,接下来按顺序给出每个case 描述,首先是两个整数 n,m(1≤n≤12, 1≤m≤40)分别表示城市数量和公路数量,下面m行每行 3个整数x,y,c 描述了一条公路的情况:x城市与y城市之间的一条公路,抢修该公路需要 c个单位时间。
注意上面所说的两城市间可能有多条公路。

输出格式:
按顺序输出每个 case 的结果,如果找不到一条合适的方案,则输出一行“impossible”,否则输出一个整数,为抢修的最优方案所需要的总时间。

输入输出样例

输入样例#1:

2
4 6
1 2 1
1 3 2
1 3 3
2 4 2
3 4 1
2 3 1
2 1
1 2 3

输出样例#1:

6
impossible

题解

参考资料:BZOJ3590【状压DP】 – CSDN博客
看到了数据范围就想到状压,但是DP并不好做。
我们想到一个边双连通分量可以从一个小的子分量加上一条链扩展而来,即把这条链通过端点的出边连接到子分量上。
我们考虑处理出$g[S][u][v]$表示状态$S$中的所有点构成一条链,端点分别是$u$和$v$的最小权值和。显然只有一个点的链权值为0,只有一条边的链权值为边权,这些可以作为DP初值。然后枚举$u$、$v$和一条$v$的出边,把出边连接的点并入链中这样转移就好。
还要处理出$h[S][u][0/1]$表示$u$到状态$S$($u \notin S$)中的任意一点的最短距离和次短距离。对于每一个$S$,枚举一个不在里面的点$u$然后枚举出边求最小值即可。
有了上面的信息,我们可以开始计算答案了,$f[S]$表示$S$内的点构成一个点双连通分量的最小权值和。我们每次往$S$中加入一条链,如果这条链是一个点,那么要加上这个点到$S$的最短和次短距离,否则加上链两个端点到$S$的最短距离和链权和。
也就是说,这个题本身是一个大力DP题。复杂度比较难看。

代码

// Code by KSkun, 2018/7
#include <cstdio>
#include <cctype>
#include <cstring>

#include <algorithm>
#include <vector>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
        ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1; register char c = fgc();
    for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
    for(; isdigit(c); c = fgc()) res = (res << 1) + (res << 3) + c - '0';
    return res * neg;
}

const int INF = 0x10101010;

int T, n, m;

struct Edge {
    int to, w;
};
std::vector<Edge> gra[15];

int bin[15], cnt[1 << 12], f[1 << 12], g[1 << 12][15][15], h[1 << 12][15][2];

int main() {
    T = readint();
    for(int i = 1; i < 15; i++) bin[i] = 1 << (i - 1);
    for(int i = 1; i < (1 << 12); i++) cnt[i] = cnt[i >> 1] + (i & 1);
    while(T--) {
        n = readint(); m = readint();
        for(int i = 1; i <= n; i++) gra[i].clear();
        for(int i = 1, u, v, w; i <= m; i++) {
            u = readint(); v = readint(); w = readint();
            gra[u].push_back(Edge {v, w});
            gra[v].push_back(Edge {u, w});
        }
        memset(g, 0x10, sizeof(g));
        for(int i = 1; i <= n; i++) {
            g[bin[i]][i][i] = 0;
        }
        for(int u = 1; u <= n; u++) {
            for(int i = 0; i < gra[u].size(); i++) {
                int v = gra[u][i].to;
                g[bin[u] | bin[v]][u][v] = std::min(g[bin[u] | bin[v]][u][v], gra[u][i].w);
            }
        }
        for(int i = 1; i < (1 << n); i++) {
            for(int u = 1; u <= n; u++) {
                for(int v = 1; v <= n; v++) {
                    if(!(i & bin[u]) || !(i & bin[v])) continue;
                    for(int j = 0; j < gra[v].size(); j++) {
                        int w = gra[v][j].to;
                        if(i & bin[w]) continue;
                        g[i | bin[w]][u][w] =
                            std::min(g[i | bin[w]][u][w], g[i][u][v] + gra[v][j].w);
                    }
                }
            }
        }
        memset(h, 0x10, sizeof(h));
        for(int i = 1; i < (1 << n); i++) {
            for(int u = 1; u <= n; u++) {
                if(i & bin[u]) continue;
                for(int j = 0; j < gra[u].size(); j++) {
                    int v = gra[u][j].to;
                    if(!(i & bin[v])) continue;
                    if(gra[u][j].w <= h[i][u][0]) {
                        h[i][u][1] = h[i][u][0];
                        h[i][u][0] = gra[u][j].w;
                    } else if(gra[u][j].w < h[i][u][1]) {
                        h[i][u][1] = gra[u][j].w;
                    }
                }
            }
        }
        memset(f, 0x10, sizeof(f));
        for(int i = 1; i <= n; i++) {
            f[bin[i]] = 0;
        }
        for(int i = 1; i < (1 << n); i++) {
            if(cnt[i] < 2) continue;
            for(int s = (i - 1) & i; s; s = (s - 1) & i) {
                int t = i - s;
                for(int u = 1; u <= n; u++) {
                    for(int v = 1; v <= n; v++) {
                        if((s & bin[u]) && (s & bin[v])) {
                            if(u == v) {
                                f[i] = std::min(f[i], f[t] + g[s][u][u] + h[t][u][0] + h[t][u][1]);
                            } else {
                                f[i] = std::min(f[i], f[t] + g[s][u][v] + h[t][u][0] + h[t][v][0]);
                            }
                        }
                    }
                }
            }
        }
        if(f[(1 << n) - 1] >= INF) {
            puts("impossible");
        } else {
            printf("%d\n", f[(1 << n) - 1]);
        }
    }
    return 0;
}