【图论】SPFA求负环

news2025/1/16 15:00:15

算法提高课笔记

文章目录

  • 基础知识
  • 例题
    • 虫洞
      • 题意
      • 思路
      • 代码
    • 观光奶牛
      • 题意
      • 思路
      • 代码
    • 单词环
      • 题意
      • 思路
      • 代码

基础知识

负环:环上权值之和是负数

求负环的常用方法 基于SPFA

  1. 统计每个点入队次数,如果某个点入队n次,则说明存在负环(完全等价于Bellman-Ford算法)
  2. 统计当前每个点的最短路中所包含的边数,如果某点的最短路包含的边数大于等于n,则说明存在负环

玄学结论:当所有点的入队次数超过2n时,就认为图中有很大肯存在负环

例题

虫洞

原题链接

农夫约翰在巡视他的众多农场时,发现了很多令人惊叹的虫洞。

虫洞非常奇特,它可以看作是一条 单向 路径,通过它可以使你回到过去的某个时刻(相对于你进入虫洞之前)。

农夫约翰的每个农场中包含 N 片田地,M 条路径(双向)以及 W 个虫洞。

现在农夫约翰希望能够从农场中的某片田地出发,经过一些路径和虫洞回到过去,并在他的出发时刻之前赶到他的出发地。

他希望能够看到出发之前的自己。

请你判断一下约翰能否做到这一点。

下面我们将给你提供约翰拥有的农场数量 F,以及每个农场的完整信息。

已知走过任何一条路径所花费的时间都不超过 10000 秒,任何虫洞将他带回的时间都不会超过 10000 秒。

输入格式

第一行包含整数 F,表示约翰共有 F 个农场。

对于每个农场,第一行包含三个整数 N,M,W。

接下来 M 行,每行包含三个整数 S,E,T,表示田地 S 和 E 之间存在一条路径,经过这条路径所花的时间为 T。

再接下来 W 行,每行包含三个整数 S,E,T,表示存在一条从田地 S 走到田地 E 的虫洞,走过这条虫洞,可以回到 T 秒之前。

输出格式

输出共 F行,每行输出一个结果。

如果约翰能够在出发时刻之前回到出发地,则输出 YES,否则输出 NO。

数据范围

1 ≤ F ≤ 5 1≤F≤5 1F5
1 ≤ N ≤ 500 , 1≤N≤500, 1N500,
1 ≤ M ≤ 2500 , 1≤M≤2500, 1M2500,
1 ≤ W ≤ 200 , 1≤W≤200, 1W200,
1 ≤ T ≤ 10000 , 1≤T≤10000, 1T10000,
1 ≤ S , E ≤ N 1≤S,E≤N 1S,EN

输入样例

2
3 3 1
1 2 2
1 3 4
2 3 1
3 1 3
3 2 1
1 2 3
2 3 4
3 1 8

输出样例

NO
YES

题意

负环板题

思路

跑一遍spfa

代码

#include <bits/stdc++.h>

using namespace std;

const int N = 510, M = 5210;

int n, m1, m2;
int h[N], ne[M], e[M], w[M], idx;
int dist[N];
int cnt[N];
bool st[N];

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

bool spfa()
{
    memset(dist, 0, sizeof dist);
    memset(cnt, 0, sizeof cnt);
    memset(st, 0, sizeof st);

    queue<int> q;
    for (int i = 1; i <= n; i ++ )
    {
        q.push(i);
        st[i] = true;
    }

    while (q.size())
    {
        int t = q.front();
        q.pop();
        st[t] = false;

        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true; // 更新次数一旦超过n就说明有负环
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    int T;
    cin >> T;
    while (T -- )
    {
        cin >> n >> m1 >> m2;
        memset(h, -1, sizeof h);
        idx = 0;

        for (int i = 0; i < m1; i ++ )
        {
            int a, b, c;
            cin >> a >> b >> c;
            add(a, b, c), add(b, a, c);
        }
        for (int i = 0; i < m2; i ++ )
        {
            int a, b, c;
            cin >> a >> b >> c;
            add(a, b, -c);
        }

        if (spfa()) puts("YES");
        else puts("NO");
    }
}

观光奶牛

原题链接

给定一张 L 个点、P 条边的有向图,每个点都有一个权值 f[i],每条边都有一个权值 t[i]。

求图中的一个环,使“环上各点的权值之和”除以“环上各边的权值之和”最大。

输出这个最大值。

注意:数据保证至少存在一个环。

输入格式

第一行包含两个整数 L 和 P。

接下来 L 行每行一个整数,表示 f[i]。

再接下来 P 行,每行三个整数 a,b,t[i],表示点 a 和 b 之间存在一条边,边的权值为 t[i]。

输出格式

输出一个数表示结果,保留两位小数。

数据范围

2 ≤ L ≤ 1000 , 2≤L≤1000, 2L1000,
2 ≤ P ≤ 5000 , 2≤P≤5000, 2P5000,
1 ≤ f [ i ] , t [ i ] ≤ 1000 1≤f[i],t[i]≤1000 1f[i],t[i]1000

输入样例

5 7
30
10
10
5
10
1 2 3
2 3 2
3 4 5
3 5 2
4 5 5
5 1 3
5 2 2

输出样例

6.00

题意

给出点权和边权,求一个环使得点权之和除以边权之和最大

思路

求比值最大的问题 -> 01分数规划 -> 二分

首先确定边界值,答案一定在[0, 1000) 之间

每次取中点mid,如果图中存在一个环使得比值大于mid,则答案在mid和r之间,反之亦然

现在问题变成了如何判断是否存在一个比值大于mid的环?

判断: ∑ f i ∑ t i > M i d \frac{\sum f_i}{\sum t_i}>Mid tifi>Mid
化简: ∑ f i − M i d ∗ ∑ t i > 0 \sum f_i-Mid*\sum t_i>0 fiMidti>0

在环上的点权我们可以任意加到边权上,对环不会有影响,假设我们将所有点权加到出边上,化简: ∑ ( f i − M i d ∗ t i ) > 0 \sum (f_i-Mid*t_i)>0 (fiMidti)>0

现在将边权全看成 f i − M i d ∗ t i f_i-Mid*t_i fiMidti

问题转换成了:图中是否存在一个正环

求最长路即可

代码

#include <bits/stdc++.h>

using namespace std;

const int N = 1010, M = 5010;

int n, m;
int wf[N];
int h[N], e[M], ne[M], wt[M], idx;
double dist[N];
int cnt[N];
bool st[N];

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

bool check(double mid)
{
    memset(dist, 0, sizeof dist);
    memset(st, 0, sizeof st);
    memset(cnt, 0, sizeof cnt);

    queue<int> q;
    for (int i = 1; i <= n; i ++ ) // 所有点存进队列
    {
        q.push(i);
        st[i] = true;
    }

    while (q.size())
    {
        int t = q.front();
        q.pop();
        st[t] = false;

        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (dist[j] < dist[t] + wf[t] - mid * wt[i])
            {
                dist[j] = dist[t] + wf[t] - mid * wt[i]; // 更新最长距离
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true; // 更新次数超过n就说明有正环
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) cin >> wf[i]; // 记录点权

    memset(h, -1, sizeof h);
    for (int j = 0; j < m; j ++ )
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }

    double l = 0, r = 1e6;
    while (r - l > 1e-4) // 误差
    {
        double mid = (l + r) / 2;
        if (check(mid)) l = mid;
        else r = mid;
    }

    printf("%.2lf\n", l);
}

单词环

原题链接

我们有 n 个字符串,每个字符串都是由 a∼z 的小写英文字母组成的。

如果字符串 A 的结尾两个字符刚好与字符串 B 的开头两个字符相匹配,那么我们称 A 与 B 能够相连(注意:A 能与 B 相连不代表 B 能与 A 相连)。

我们希望从给定的字符串中找出一些,使得它们首尾相连形成一个环串(一个串首尾相连也算),我们想要使这个环串的平均长度最大。

如下例:

ababc
bckjaca
caahoynaab

第一个串能与第二个串相连,第二个串能与第三个串相连,第三个串能与第一个串相连,我们按照此顺序相连,便形成了一个环串,长度为 5+7+10=22(重复部分算两次),总共使用了 3 个串,所以平均长度是 223≈7.33。

输入格式

本题有多组数据。

每组数据的第一行,一个整数 n,表示字符串数量;

接下来 n 行,每行一个长度小于等于 1000 的字符串。

读入以 n=0 结束。

输出格式

若不存在环串,输出”No solution”,否则输出最长的环串的平均长度。

只要答案与标准答案的差不超过 0.01,就视为答案正确。

数据范围

1 ≤ n ≤ 105 1≤n≤105 1n105

输入样例

3
intercommunicational
alkylbenzenesulfonate
tetraiodophenolphthalein
0

输出样例

21.66

题意

n个字符串,如果a的末尾两个字符和b的开头相同则能相连,求能构成的环的平均长度最大值

思路

把每个字符串的首位两个字母看做一个点,比如说样例可以这样建图:
在这里插入图片描述
答案就变成了求 ∑ w i ∑ 1 \frac{\sum w_i}{\sum 1} 1wi的最大值

答案在(0, 1000)之内,二分做,类似观光奶牛

最终判断式为: ∑ w i − M i d ∗ ∑ 1 > 0 \sum w_i - Mid*\sum 1>0 wiMid1>0

判断有没有解直接把 Mid = 0 代入即可,因为如果等于0都不能满足的话大于0就更不会满足了

于是成功TLE,用一下玄学优化

代码

#include <bits/stdc++.h>

using namespace std;

const int N = 700, M = 100010;

int n;
int h[N], e[M], ne[M], w[M], idx;
double dist[N];
int cnt[N];
bool st[N];

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

bool check(double mid)
{
    memset(st, 0, sizeof st);
    memset(cnt, 0, sizeof cnt);

    queue<int> q;
    for (int i = 0; i < 676; i ++ ) // 所有点存入队列
    {
        q.push(i);
        st[i] = true;
    }

    int count = 0;
    while (q.size())
    {
        int t = q.front();
        q.pop();
        st[t] = false;

        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (dist[j] < dist[t] + w[i] - mid)
            {
                dist[j] = dist[t] + w[i] - mid;
                cnt[j] = cnt[t] + 1;
                if (++ count > 10000) return true; // 次数过多直接返回(玄学可能失败
                if (cnt[j] >= N) return true; // 这个是保证一定正确的
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

int main()
{
    char str[1010];
    while (scanf("%d", &n), n)
    {
        memset(h, -1, sizeof h);
        idx = 0;
        for (int i = 0; i < n; i ++ )
        {
            cin >> str;
            int len = strlen(str);
            if (len >= 2)
            {
                // 用类似于26进制的方法存储
                int left = (str[0] - 'a') * 26 + str[1] - 'a';
                int right = (str[len - 2] - 'a') * 26 + str[len - 1] - 'a';
                add(left, right, len);
            }
        }

        if (!check(0)) puts("No solution"); // 判断是否有解
        else
        {
            double l = 0, r = 1000;
            while (r - l > 1e-4) // 误差
            {
                double mid = (l + r) / 2;
                if (check(mid)) l = mid;
                else r = mid;
            }
            printf("%lf\n", r);
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/998192.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OSPF路由计算

1、Router LSA LSA 链路状态通告&#xff0c;是OSPF进行路由计算的主要依据&#xff0c;在OSPF的LSU报文中携带&#xff0c;其头重要字段及解释&#xff1a; LS Type&#xff08;链路状态类型&#xff09;&#xff1a;指示本LSA的类型。 在域内、域间、域外…

upload-labs/Pass-07 未知后缀名解析漏洞复现

upload-labs/Pass-07 漏洞复现 页面&#xff1a; 我们看到有一个图片上传功能。 我们上传一个png文件发现能够成功上传&#xff0c;那其他文件呢&#xff0c;如php文件。 我们看一下是否能上传一个php文件&#xff1a; php文件内容&#xff1a; <?phpeval($_REQUEST[]…

计算机系统的基本概念

计算机系统的基本概念 本文主要以hello.c这个程序的整个生命周期来简单了解一下计算机系统结构的基本概念。 #include <stdio.h>int main() {printf("hello, world\n");return 0; }gcc hello.c -o hello ./hello hello, world此刻&#xff0c;hello.c源程序…

运算符,switch

目录 算术运算符 逻辑运算符 强制类型转换 自增自减运算符 ​编辑 三目运算符 A&#xff1f;B:C 逗号表达式 switch 算术运算符 除法的运算结果和运算对象的数据类型有关&#xff0c;两个都是int商就是int&#xff0c;被除数或者除数只要有一个是浮点型数据&#xff0c;…

ARM DIY(十一)板子名称、开机 logo、LCD 控制台、console 免登录、命令提示符、文件系统大小

文章目录 前言板子名称uboot Modelkernel 欢迎词、主机名 开机 logoLCD 控制台console 免登录命令提示符文件系统大小 前言 经过前面十篇文章的介绍&#xff0c;硬件部分调试基本完毕&#xff0c;接下来的文章开始介绍软件的个性化开发。 板子名称 uboot Model 既然是自己的…

最新2米分辨率北极开源DEM数据集(矢量文件)

一、项目背景 美国明尼苏达大学(University of Minnesota)的极地地理空间中心(Polar Geospatial Center, PGC)于2023年8月发布了北极数字高程模型4.1版本(ArcticDEM Mosaic 4.1)。该DEM数据集是革命性的&#xff0c;分辨率达到了2米&#xff0c;而一般的开源DEM数据集分辨率是3…

代码随想录算法训练营第十八天|513. 找树左下角的值|112. 路径总和|106. 从中序与后序遍历序列构造二叉树

513. 找树左下角的值 题目&#xff1a;给定一个二叉树的 根节点 root&#xff0c;请找出该二叉树的 最底层 最左边 节点的值。 假设二叉树中至少有一个节点。 示例 1: 输入: root [2,1,3] 输出: 1 思路一&#xff1a;层序遍历&#xff0c;最后一层的第一个元素&#xff0c;即…

【GAMES202】Real-Time Ray Tracing 1—实时光线追踪1

一、前言 这篇我们开始新的话题—Real-Time Ray Tracing简称RTRT&#xff0c;也就是实时光线追踪&#xff0c;关于光线追踪&#xff0c;我们已经不止一次提到过它的优点&#xff0c;无论是软阴影还是全局光照&#xff0c;光线追踪都很容易做&#xff0c;唯一的缺点就是速度太慢…

时空预测 | 线性时空预测模型、图时空预测

目录 线性时空预测图时空预测 线性时空预测 这篇文章在时空预测领域&#xff0c;搭建了一个简单高效的线性模型&#xff0c;且使用了channel-independence的方式进行建模。 模型的整体结构如下图所示&#xff0c;是一个级联的结构。输入分为三个部分&#xff1a;temporal embed…

java的动态代理如何实现

一. JdkProxy jdkproxy动态代理必须基于接口(interface)实现 接口UserInterface.java public interface UserService {String getUserName(String userCde); }原始实现类&#xff1a;UseServiceImpl.java public class UserServiceImpl implements UserSerice {Overridepub…

布局过程的完全解析

前言 那么为什么要分为两个流程呢 因为测量流程是一个复杂的流程&#xff0c;有时候不一定一遍就能得出测量结果&#xff0c;可能需要 2 - 3 次甚至更多 自定义布局的几种类型&#xff0c;也是自定义布局的两个方法 实战&#xff0c;第一种类型&#xff1a;改写已有View 的步骤…

day34 Map Properties

Map<String,Integer> map new HashMap<>(); map.put("a",1);map.put("b",2);map.put("c",3);map.put("d",4);Integer a map.put("a", 2);System.out.println(a);Integer chinese map.put("语文",1…

新一代G7系列浪潮云海超融合EC纠删功能设计

浪潮云海在2023年5月正式发布新一代InCloud Rail G7系列超融合一体机&#xff0c;其内置的InCloud dSAN超融合存储组件&#xff0c;基于新一代的硬件平台设计&#xff0c;支持全栈RDMA协议&#xff0c;同时在EC纠删功能上也带来全新体验&#xff0c;为新时代用户提供更丰富的产…

PYTHON(一)——认识python、基础知识

一、为什么要学习python&#xff1f; Python 被认为是人工智能、机器学习的首选语言&#xff0c;可以说是全世界最流行通用范围最广的语言&#xff0c;几乎可以完成所有的任务&#xff0c;像设计游戏、建网站、造机器人甚至人工智能等都广泛使用Python。 二、输出&#xff08;…

注解-宋红康

目录 一、注解&#xff08;Annotation&#xff09;概述二、常见的注解实例三、如何自定义注解四、JDK中的四个元注解五、Java8注解的新特性1、可重复注解2、类型注解 一、注解&#xff08;Annotation&#xff09;概述 二、常见的注解实例 三、如何自定义注解 自定义注解必须配…

查询硬盘序列号、物理地址及对应批处理命令

首先说明&#xff1a; 通过winR -> cmd -> diskpart -> list disk -> select disk 0 -> detail disk -> 然后显示磁盘ID等&#xff0c;这不是序列号&#xff0c;只是磁盘ID而已。 查询序列号命令很简单&#xff1a; wmic diskdrive get serialnumber或者 w…

权限、认证与授权

权限、认证与授权 1、权限概述 &#xff08;1&#xff09;什么是权限 权限管理&#xff0c;一般指根据系统设置的安全策略或者安全规则&#xff0c;用户可以访问而且只能访问自己被授权的资源&#xff0c;不多不少。权限管理几乎出现在任何系统里面&#xff0c;只要有用户和…

webstorm HbuilderX工具未配置

问题&#xff1a;调试动迁uni app h5项目&#xff0c;报错 webstorm是换了电脑新安装&#xff0c; HBuilerx是从旧电脑拷贝过来的解压的文件 解决&#xff1a; 把uniapp插件&#xff0c;卸载 再重启webstorm,重装安装uniapp Tool&#xff0c; 安装第一个&#xff0c;免费。…

拓展外部SRAM

外部拓展芯片 IS62WV51216A 芯片手册 支持高速时钟通道时间为45、55ns 芯片引脚定义 通道时序 读定义表 一个纵列表示当前使用的高速通道的时间&#xff0c;选一个纵列作为参数标准。 地址控制读时序 如图&#xff0c;大概需要三个参数 写时序定义表 还是选择55ns参数 写…

数据接口工程对接BI可视化大屏(五)数据接口发布

文章目录 第5章 数据接口发布5.1 编写Service5.2 从MySQL中返回数据5.2.*1 封装Bean5.2.*2 编写Mapper5.2.3 编写ServiceImpl5.2.4 编写Controller5.2.5 测试 5.3 从Redis中返回数据5.3.1 封装Bean5.3.2 编写Mapper5.3.3 编写ServiceImpl5.3.4 编写Controller5.3.5 测试 5.4 从…