• {{item}}
  • {{item}}
  • {{item}}
  • {{item}}
  • 天祈
  • {{item.name}}
  • {{item.name}}
  • {{item.name}}
  • {{item.name}}

算法专题——SPFA拓展

SPFA算法概念

SPFA算法可以归结为两个特点:

  1. 可以松弛操作进行更新的点进行更新,并将更新的点加入队列(如果队列没有该点的话)以更新其他的点.
  2. while循环结束的条件,所有边均满足松弛定理不能再更新.

当图中路径存在负环时,最短路是求不出来的,对应到图中即: 每一个节点不存在一个固定的解使得所有边对应的松弛操作不可能同时满足.

路径存在最短路是一个重要的性质, 与此同时路径中的负环同样具有十分重要的性质.



求负环

求负环的几种判断方式优化方式以及对应代码:

//一个点的入队次数不超过n次, 超过n次就说明存在负环.
if (++ cnt[i] > n) return false;
//最短路包含的边数不超过n次, 超过n次就说明存在负环. 		更加推荐这种方法
cnt[i] = cnt[j] + 1;
if (cnt[i] > n) return false;
//不一定正确, 当所有点的入队次数超过一定阈值时说明存在负环
if (++cnt > 2 * MAXN) return false;
//使用堆栈进行存储									堆栈的特点更适合找负环的需求

求负环的重点, SPFA算法必须要可以遍历到所有的边. 因此要根据需求选择是检测图中所有的负环还是路径上的负环.

如果需要检测图中所有的负环, 且不能使起点到达所有点的情况, 就需要将所有的点入队, 并设置好dist值的大小.

例题

wormhole

题面:

分析:

判断是否存在一个农场使得从该农场出发, 最终可以回到该农场, 并完成时光倒流.

即判断图中是否存在负环. 由于并没有说该图是一个连通图, 所以需要将所有点提前压入队列. 下面给出核心代码:

const int MAXN = 510, MAXM = 6010;

int h[MAXN], e[MAXM], val[MAXM], ne[MAXM], idx;
int dist[MAXN], cnt[MAXN];
bool vis[MAXN];
queue<int> que;
bool SPFA(int start) {
    memset(dist, 0x3f, sizeof dist);
    memset(cnt, 0, sizeof cnt);
    memset(vis, false, sizeof vis);

    dist[0] = 0; vis[0] = true;
    while (!que.empty()) que.pop();
    que.push(0);

    while (!que.empty()) {
        int u = que.front(); que.pop();
        vis[u] = false;
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            if (dist[v] > dist[u] + val[i]) {
                dist[v] = dist[u] + val[i];
                cnt[v] = cnt[u] + 1;
                if (cnt[v] > n) return true;
                if (!vis[v]) {
                    que.push(v);
                    vis[v] = true;
                }
            }
        }
    }
    return false;
}


零一分数规划

求负环的特殊情况, 所有形如求\(\frac{\sum边权1}{\sum边权2}\)最大/小值的题目我们称之为零一分数规划问题, 该问题一般通过SPFA配合二分解决问题. 直接通过例题进行分析.

例题

sightseeing cows

题面:

分析:

该题就是一道典型的零一分数规划问题. 由于奶牛最终要回到起点, 所以奶牛最终应是在一个环上进行观光. 又所谓幸福度不会因为叠加计算是为了防止重复绕圈计算游览一圈可以得到的最大值. 即问题转化为要找到一个环, 使得满足公式\(\frac{点权}{边权} > 比例\) 的同时, 比例最大. 而在一个环中, 点权可以转换为边权(出边或者入边都可以), 所以和\(\frac{\sum边权1}{\sum边权2}\)的形式差不多, 是一道求零一分数规划的问题.

将公式进行一个变形得到: 点权 - 边权 * 比例 > 0, 可以发现此时就变成了一个求一个最大的比例, 使得图中存在存在一个正环即可. 如果得到一个比例, 我们可以通过最长路求正环的方法验证得到的比例是否满足条件, 不难发现比例在数值范围内是满足单调性的, 所以可以通过二分比例, 然后通过最长路求正环的方法进行检测, 从而得到比例的最大值.

下面和核心代码:

int h[MAXN], e[MAXM], val[MAXM], ne[MAXM], idx;
int pval[MAXN];												//点权
double dist[MAXN];
queue<int> que;
int cnt[MAXN];
bool vis[MAXN];

bool Check(double mid) {
    memset(vis, true, sizeof vis);
    memset(cnt, 0, sizeof cnt);
    while (!que.empty()) que.pop();

    for (int i = 1; i <= n; i++) que.push(i);
    while (!que.empty()) {
        int u = que.front(); que.pop();
        vis[u] = false;
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            if (dist[v] < dist[u] - val[i] * mid + pval[u]) {
                dist[v] = dist[u] - val[i] * mid + pval[u];
                if (++cnt[v] > n) return true;					//通过记录入队次数的判断正环的方法
                if (!vis[v]) {
                    vis[v] = true;
                    que.push(v);
                }
            }
        }
    }
    return false;
}

int main {
	double l = 0, r= 1010;				//注意边界情况, 奶牛至少要去两个景点回到起点, 即一定要找到环, 由于数据保证一定存在环, 且比例总是部位负数, 所以可以让l = 0
	while (r - l > 1e-4) {				//题目要求的精度再小两位
		double mid = (l + r) / 2;
		if (Check(mid)) l = mid;
		else r = mid;
	}
	cout << r << endl;
}

Word Rings

题面:

分析:

这道题的难点在于建图的方式, 一般很容易直接将单词看作一个节点, 在单词与单词之间建图, 但是不难发现, 这样无论是需要建的点的数量还是边的数量都是一个十分大的数量级, 空间显然不够用, 就算空间够用, 光是建图的时间也会超时, 所以显然这种直接了当的建图方式是不可取的.

我们可以考虑在词缀与词缀之间进行建图, 词缀1→词缀2表示的是以词缀1开头, 词缀2结尾的一个单词, 边权为单词的长度. 基于次我们可以发现这种建图的方式所需要的点只有不到300个, 所建成的边也只有O(n)级别. 所以在这类图论题中, 需要留意图的建立方式.

然后就是很普通的求解答案了, 下面贴出全部代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;

const int MAXM = 1e5 + 10, MAXN = 700;

int n;
int h[MAXN], e[MAXM], val[MAXM], ne[MAXM], idx;
double dist[MAXN];
queue<int> que;
int cnt[MAXN];
bool vis[MAXN];
char tmp[MAXM];

void AddEdge(int a, int b, int c) {
    e[idx] = b, val[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

bool Check(double mid) {
    int count = 0;
    memset(cnt, 0, sizeof cnt);
    memset(vis, true, sizeof vis);
    while (!que.empty()) que.pop();

    for (int i = 0; i < 676; i++) que.push(i);

    while (!que.empty()) {
        int u = que.front(); que.pop();
        vis[u] = false;
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            if (dist[v] < dist[u] + val[i] - mid) {
                dist[v] = dist[u] + val[i] - mid;
                cnt[v] = cnt[u] + 1;
                if (++count >= 10000) return true;
                if (cnt[v] >= MAXN) return true;
                if (!vis[v]) {
                    que.push(v);
                    vis[v] = true;
                }
            }
        }
    }
    return false;
}

int main() {
    while (scanf("%d", &n), n) {
        memset(h, -1, sizeof h);
        idx = 0;
        for (int i = 0; i < n; i++) {
            scanf("%s", tmp);
            int len = strlen(tmp);
            if (len >= 2) {												//注意边界情况!
                int left = (tmp[0] - 'a') * 26 + tmp[1] - 'a';			//通过26进制的方法进行节点存储
                int right = (tmp[len - 2] - 'a') * 26 + tmp[len - 1] - 'a';
                AddEdge(left, right, len);                
            }
        }
        if (!Check(0)) puts("No solution.");
        else {
            double l = 0, r = 1010;
            while (r - l > 1e-4) {
                double mid = (l + r) / 2;
                if (Check(mid)) l = mid;
                else r = mid;
            }
            printf("%lf\n", r);            
        }
    }
    return 0;
}


差分约束

对一个不含负环的连通图求最短路, 当SPFA函数执行完之后, 可以发现任意取一个点, 所有的入边都满足三角不等式dist[u] + val[i] >= dist[v], 一个入边就对应着一个这样的三角不等式, 而该点的取值dist[v]为了使所有的不等式成立, 应取所有不等式里的最小值, 这个值是到节点v的最短路, 同时也是dist[v]可以取到的最大值. 即如果我们将视角从求到v的最短路转移到求v的最大值上, 将图论的知识点迁移到数学表达式上, 我们便得到了一种新的知识点----差分约束.

差分约束就是利用图论中的三角不等式以求解在满足所有的限制(以不等式组的形势给出)的同时, 变量可以取到的最大/小值.


**差分约束的易错点: **

  1. 要注意这里取到的最大/小值是相对于起点(起点表示的变量)而言的, 不难发现, 当得到一组可行解的时候, 我们往往(除了一些特殊的不等式)可以通过所有变量加减一个数得到另一组可行解. 这里求出的最大值最小值是相对于起点而言的, 就如用SPFA求最短路一样, 求出的解是单源最短路, 只能确定起点start到目标节点v的差值是最大/小值, 不能确定一个非起点节点u到非起点目标节点v的最大/小值.
  2. 差分约束一个十分重要的点, 需要将节点之间所有可能包含的关系都用不等式表达出来, 比如下面的例题----Intervals和cashier employment

差分约束的几个重点:

  1. 差分约束存在负环: 即没有一个固定的dist值满足给定的三角不等式, 即无解. 求解不等式是否有解, 可以参照求负环的步骤, 建立虚拟原点

  2. dist[v] == INF: 即从地点start到目标节点v之间不存在最大/小值, v不受start控制, 最大值为正无穷.

  3. 求最小值使用最长路,求最大值使用最短路: 从起点到目标节点可以得到n条途径, 目标节点的取值应该满足所有的不等式, 最长路三角不等式为dist[u] + val[i] <= dist[v], 可以发现当满足所有不等式时, dist[v]会取得可以取得的值里边的最小值. 最短路同理.

  4. 一些特殊条件的转换: u > v可以转化为u >= v + 1; u == v可以转化为u >= v, v >= u; u == c可以设立一个节点dist[0] == c, 然后仿照第二个式子的转换.


例题

Candies

题面:

分析:

模板题, 要求最大值, 使用最短路, 注意建边的方式, 下面给出代码, 以及在注释中给出注意事项.

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cmath>
#define debug(x) cout << #x << " = " << x << endl
using namespace std;

const int MAXN = 3e4 + 10, MAXM = 15e4 + 10;

int n, m;
int h[MAXN], e[MAXM], val[MAXM], ne[MAXM], idx;
int dist[MAXN];
bool vis[MAXN];
struct Node {
    int v, val;
    Node(int _v = 0, int _val = 0) : v(_v), val(_val) {}
    bool operator< (const Node& a) const {return val > a.val;}
}tmp;

void AddEdge(int a, int b, int c) {
    e[idx] = b, val[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
//求最大值→最短路
void dijkstra() {								//由于SPFA会超时, 加上不存在不边, 所以这里使用dijkstra算法求最短路
    memset(dist, 0x3f, sizeof dist);			
    memset(vis, false, sizeof vis);
    priority_queue<Node> que;
    dist[1] = 0;								//起点设为0, 之后输出答案可以直接输出dist[n]
    que.push(Node(1, 0));
    while (!que.empty()) {
        tmp = que.top(), que.pop();
        int u = tmp.v;;
        if (vis[u]) continue;
        vis[u] = true;
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            if (!vis[v] && dist[v] > dist[u] + val[i]) {
                dist[v] = dist[u] + val[i];
                que.push(Node(v, dist[v]));
            }
        }
    }
}

int main() {
    scanf("%d%d", &n, &m);
    int a, b, c;
    memset(h, -1, sizeof h);
    for (int i = 0; i < m; i++) {
        scanf("%d%d%d", &a, &b, &c);
        AddEdge(a, b, c);
    }
    dijkstra();
    cout << dist[n] << endl;
}

Intervals

题面:

分析:

这道题中, 仍可以较为容易的找到关系, 得到数组dist[i]表示从1~i之间, 该集合中总共有多少个元素, 这样我们就可以用不等式来表示区间的含义了, 即dist[r] - dist[l - 1] >= interval[i], 在此基础上, 将节点与节点所有可能包含的条件都塞进去, 便可以得到我们最终的表达式(求最小值, 用最长路):

1. dist[r] >= dist[l - 1] + interval[i]
2. dist[i] >= dist[i - 1]
3. dist[i - 1] >= dist[i] - 1		//2 3表示i - 1与i之间的差距只能在1之间

建图代码如下, 求最长路代码略:

memset(h, -1, sizeof h);
for (int i = 0; i < n; i++) {
    scanf("%d%d%d", &a, &b, &c);
    MAX = max(MAX, b);
    AddEdge(a - 1, b, c);
}
for (int i = 1; i <= MAX; i++) {
    AddEdge(i - 1, i, 0);
    AddEdge(i, i - 1, -1);
}

Layout

题面:

分析:

这道题题目需要判断是否有解, 以及判断是否距离可以无限, 其余方面倒没什么特殊的, 见下面的关键代码:

bool SPFA(int size) {
    memset(dist, 0x3f, sizeof dist);
    memset(vis, false, sizeof vis);
    memset(cnt, 0, sizeof cnt);

    queue<int> que;
    for (int i = 1; i <= size; i++) {	//通过传递一个size参数, 判断是否要将所有点压入队列, 从而在求最短路和求负环之间进行切换
        que.push(i); vis[i] = true;
        dist[i] = 0;
    }

    while (!que.empty()) {
        int u = que.front(); que.pop();
        vis[u] = false;
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            if (dist[v] > dist[u] + val[i]) {
                dist[v] = dist[u] + val[i];
                cnt[v] = cnt[u] + 1;
                if (cnt[v] >= n) return false;
                if (!vis[v]) {
                    que.push(v); vis[v] = true;
                }
            }
        }
    }
    return true;
}
int main() {
	建图();
    //建图完毕后, 求最短路
    if (!SPFA(n)) puts("-1");	
    else {
        SPFA(1);
        if (dist[n] == INF) puts("-2");
        else cout << dist[n] << endl;
    }
}


Cashier Employment

题面:

分析:

差分约束类型的问题中, 还有要一个难点在于知道这道题是一道差分约束的题, 而确定一道题时差分约束的题, 则需要找到题目中的不等式, 有些题目比较隐藏差分约束的特点, 这时候就可以通过先不管差分约束的一般形式, 有限找到不等式为主.

这道题而言, 我们设从i - 1点开始工作的收银员有x[i]个, i-1 ~ i点需要的收银员需要num[i]个, 那么就可以得到x[i - 7] + x[i - 6] + ... + x[i] >= num[i], 这是一个不等式, 虽然不是我们熟悉的两个变量的不等式, 不过是一个好兆头, 接下来我们可以发现, 可以通过求前缀和的方法进行化简, 于是我们设sum[i] = Σx[i] , 这样就得到了不等式sum[i] - sum[i - 8] >= num[i] , 当然还要考虑一些边界情况, 于是我们可以得到所有的不等式(求最小值, 用最长路):

1. sum[i - 1] >= sum[i] - cash[i]				//cash[i]表示i - 1时间点开始工作的收银员, 最多可以招聘的个数
2. sum[i] >= sum[i - 1]
3. sum[i]  >= sum[i - 8] + num[i]
4. sum[i] >= sum[i + 16] + num[i] - sum[24]			//不难发现, 这里有一个额外的变量, sum[24], 即最终的答案不过好在数据的范围并不大, 我们可以枚举sum[24]的值, 将其视为一个常量
5. sum[24] >= sum[0] + ans, sum[0] >= sum[24] - ans, sum[0] = 0		//建立一个虚拟原点, 用于限定sum[24]的值, 是基于4而添加的条件	

代码:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#define debug(x) cout << #x << " = " << x << endl
using namespace std;

const int MAXN = 25, MAXM = 200 + 10;

int t, n;
int num[MAXN], cash[MAXN], sum[MAXN];
int h[MAXN], e[MAXM], val[MAXM], ne[MAXM], idx;
int cnt[MAXN];
bool vis[MAXN];

void AddEdge(int a, int b, int c) {
    e[idx] = b, val[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void Build(int s24) {
    memset(h, -1, sizeof h);
    idx = 0;
    for (int i = 1; i <= 24; i++) AddEdge(i - 1, i, 0), AddEdge(i, i - 1, -cash[i]);
    for (int i = 8; i <= 24; i++) AddEdge(i - 8, i, num[i]);
    for (int i = 1; i < 8; i++) AddEdge(16 + i, i, num[i] - s24);
    AddEdge(0, 24, s24); AddEdge(24, 0, -s24);
}

bool SPFA(int s24) {
    Build(s24);							//由于每一次枚举的sum[24]都不一样, 所以每一次枚举都要重新建图
    memset(cnt, 0, sizeof cnt);
    memset(sum, -0x3f, sizeof sum);
    memset(vis, false, sizeof vis);

    queue<int> que;
    que.push(0); vis[0] = true;
    sum[0] = 0;

    while (!que.empty()) {
        int u = que.front(); que.pop();
        vis[u] = false;
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            if (sum[v] < sum[u] + val[i]) {
                sum[v] = sum[u] + val[i];
                cnt[v] = cnt[u] + 1;
                if (cnt[v] >= 25) return false;         
                if (!vis[v]) {
                    que.push(v); vis[v] = true;
                }
            }
        }
    }
    return true;
}

int main() {
    scanf("%d", &t);
    while (t--) {
        for (int i = 1; i <= 24; i++) scanf("%d", &num[i]);
        scanf("%d", &n);
        int tmp;
        memset(cash, 0, sizeof cash);
        for (int i = 0; i < n; i++) {
            scanf("%d", &tmp);
            cash[tmp + 1]++;
        }
        int flag = 0;
        for (int i = 0; i <= 1000; i++) {				//枚举答案
            if (SPFA(i)) {
                cout << i << endl;
                flag = 1;
                break;
            }
        }
        if (!flag) puts("No Solution");        
    }
    return 0;
}
posted @ 2021-08-19 16:40  TanJI_C  阅读(56)  评论(0编辑  收藏  举报