【学习笔记】图论总体学习笔记
记录图论的学习。
省流:被吊打了
更改日志
2024/01/08:开坑,先不写。
2024/01/09:更新非严格次短路、判负环、差分约束系统、最短路计数、传递闭包。
学习背景
图论早就学了,只是把知识记一遍而已,有些太简单的就不记了。
最短路技巧
最短路大家应该都会,我就来写几个技巧。
1.非严格次短路
非严格次短路就是如果有多条最短路,次短路就是最短路。
先看例题吧,边讲例题边讲做法。
例题
P2865 [USACO06NOV] Roadblocks G
又是一道蓝题!
贝茜把家搬到了一个小农场,但她常常回到 FJ 的农场去拜访她的朋友。贝茜很喜欢路边的风景,不想那么快地结束她的旅途,于是她每次回农场,都会选择第二短的路径,而不象我们所习惯的那样,选择最短路。
贝茜所在的乡村有
贝茜选择的第二短的路径中,可以包含任何一条在最短路中出现的道路,并且,一条路可以重复走多次。当然咯,第二短路的长度必须严格大于最短路(可能有多条)的长度,但它的长度必须不大于所有除最短路外的路径的长度。
解法
这里可以看出不就是个非严格次短路,就是如果有多条最短路,次短路就是最短路。
那么我们就可以想,能不能再求最短路时求出次短路呢?答案是肯定的。
我用的是 dijkstra
堆优化( dijkstra
好闪,拜谢 dijkstra
!)。
先设
更新时有以下三种情况:
-
如果
,说明从按 的最短路再走到点 比原先的最短路更优,则更新最短路,就是 。 -
如果
且 ,说明从按 的最短路再走到点 比原先的次短路更优,则更新次短路,就是 。 -
如果
,说明从按 的次短路再走到点 还比原先的次短路更优,则再更新次短路,就是 。
若上述条件有一个成立,则将点
设最后一个农场为点
CODE(就是非严格次短路模板啦):
#include<bits/stdc++.h>
#define fi first
#define se second
using namespace std;
int r,n,u,v,w;
vector<pair<int,int> >g[100010];//还要存上权值
int dis[10010],ret[10010];
struct node{
int fi,se;//节点和最短路
bool operator<(const node &b) const{//重载运算符,大根堆->小根堆
return se>b.se;
}
};
priority_queue<node>q;//堆
pair<int,int> m_p(int x,int y){
return make_pair(x,y);
}void dij(){
memset(dis,127,sizeof dis);//初始化
memset(ret,127,sizeof ret);
dis[1]=0;
q.push((node){1,0});//先入堆
while(!q.empty()){
int u=q.top().fi,dic=q.top().se
q.pop();
for(int i=0;i<g[u].size();i++){
int v=g[u][i].fi,w=g[u][i].se;
int f=0;
if(dic+w<dis[v]){//转移
dis[v]=dic+w,f=1;
}if(dic+w>dis[v]&&dic+w<ret[v]){
ret[v]=dic+w,f=1;
}if(ret[u]+w<ret[v]){
ret[v]=ret[u]+w,f=1;
}if(f==1){//有成立的条件,入队
q.push((node){v,dis[v]});
}
}
}
}int main(){
scanf("%d%d",&r,&n);
for(int i=1;i<=n;i++){
scanf("%d%d%d",&u,&v,&w);
g[u].push_back(m_p(v,w));
g[v].push_back(m_p(u,w));
}dij();
printf("%d",ret[r]);
return 0;
}
2.图的判负环
巨大的知识点。
这里我们知道,dijkstra
就是无法解决边权为负数的问题,但是其他的三个 Floyd
、Bellman-Ford
和 SPFA
(死了!)可以,还可以判负环,接下来我将用一道例题来讲一下这三种算法的判负环。
首先,先看一下如何判负环。
我们可以发现,若 Floyd
中 实体负环的存在。
再来看看 Bellman-Ford
,如果它没有负环的话,进行了
最后再是 SPFA
,这里如果某顶点的入队次数超过 的卡 图,使得入队次数接近 SPFA
的
例题
P3385 【模板】负环
题目描述
给定一个
负环的定义是:一条边权之和为负数的回路。
输入格式
本题单测试点有多组测试数据。
输入的第一行是一个整数
第一行有两个整数,分别表示图的点数
接下来
- 若
,则表示存在一条从 至 边权为 的边,还存在一条从 至 边权为 的边。 - 若
,则只表示存在一条从 至 边权为 的边。
输出格式
对于每组数据,输出一行一个字符串,若所求负环存在,则输出 YES
,否则输出 NO
。
提示
数据规模与约定
对于全部的测试点,保证:
, 。 , 。 。
提示
请注意,
解法(代码部分,前面已经讲过方法)
Floyd
做法(不优)
这里的 Floyd
这样可以拿到
CODE:
#include<bits/stdc++.h>
using namespace std;
int t,n,m,u,v,w,dis[2010][2010];
void floyd(){
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(dis[i][k]==0x3f3f3f3f||dis[k][j]==0x3f3f3f3f){
continue;
}dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);转移
if(dis[1][i]!=0x3f3f3f3f&&dis[i][i]<0){//判断负环
printf("YES\n");
return;
}
}
}
}printf("NO\n");
}int main(){
scanf("%d",&t);
while(t--){
memset(dis,0x3f3f3f3f,sizeof dis);
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d%d",&u,&v,&w);
if(w>=0){
dis[u][v]=min(w,dis[u][v]);//建图
dis[v][u]=min(w,dis[v][u]);
}else{
dis[u][v]=min(w,dis[u][v]);
}
}floyd();
}return 0;
}
Bellman-Ford
做法
注意初始化,前面已经讲过如何判负环,满分。
CODE:
#include<bits/stdc++.h>
using namespace std;
int t,n,m,u,v,w,dis[2010],cnt[2010],vis[2010];
struct node{
int v,w;
};
vector<node>g[2010];
void Bellman_Ford(int a){
memset(dis,0x3f,sizeof dis);
dis[a]=0;
for(int i=1;i<n;i++){
for(int j=1;j<=n;j++){
if(dis[j]!=0x3f3f3f3f){
for(int k=0;k<g[j].size();k++){
int v=g[j][k].v,w=g[j][k].w;
if(dis[j]+w<dis[v]){//尝试松弛
dis[v]=dis[j]+w;
}
}
}
}
}for(int j=1;j<=n;j++){
for(int k=0;k<g[j].size();k++){
int v=g[j][k].v,w=g[j][k].w;
if(dis[j]==0x3f3f3f3f||dis[v]==0x3f3f3f3f){//判断
continue;
}if(dis[j]+w<dis[v]){
printf("YES\n");
return;
}
}
}printf("NO\n");
}int main(){
scanf("%d",&t);
while(t--){
scanf("%d%d",&n,&m);
for(int i=1;i<=2000;i++){
g[i].clear();
}for(int i=1;i<=m;i++){
scanf("%d%d%d",&u,&v,&w);
if(w>=0){
g[u].push_back((node){v,w});//建图
g[v].push_back((node){u,w});
}else{
g[u].push_back((node){v,w});
}
}Bellman_Ford(1);
}return 0;
}
SPFA
做法
前面同样已经讲过如何判负环,满分。
CODE:
#include<bits/stdc++.h>
using namespace std;
int t,n,m,u,v,w,dis[2010],cnt[2010],vis[2010];
struct node{
int v,w;
};
vector<node>g[2010];
void spfa(int a){
memset(dis,0x3f,sizeof dis);//初始化
memset(cnt,0,sizeof cnt);
memset(vis,0,sizeof vis);
queue<int>q;
dis[a]=0,vis[a]=1,q.push(a);
while(!q.empty()){
int u=q.front();
vis[u]=0;
q.pop();
for(int i=0;i<g[u].size();i++){
int v=g[u][i].v,w=g[u][i].w;
if(dis[u]+w<dis[v]){//操作
dis[v]=dis[u]+w;
cnt[v]=cnt[u]+1;//统计边的条数
if(cnt[v]>=n){//如果边的数量超过了n
printf("YES\n");
return;
}if(vis[v]==0){//入队
vis[v]=1;
q.push(v);
}
}
}
}printf("NO\n");
}int main(){
scanf("%d",&t);
while(t--){
scanf("%d%d",&n,&m);
for(int i=1;i<=2000;i++){
g[i].clear();//初始化
}for(int i=1;i<=m;i++){
scanf("%d%d%d",&u,&v,&w);
if(w>=0){//建图
g[u].push_back((node){v,w});
g[v].push_back((node){u,w});
}else{
g[u].push_back((node){v,w});
}
}spfa(1);
}return 0;
}
3.差分约束系统
差分约束系统是一种特殊的 图论强大),这里包含了
这个
-
无解。
-
可以找到其中一组解
,再将其进行常数变换,进而得到无数组解(怎么开始讲数学了)。
总所周知,我们的最短路若不存在负环,则
于是,我们就可以将约束条件中的变量
抽象了图模型后,我们可以建立一个顶点
这样就可以从
从
例题
P1993 小 K 的农场
题目描述
小 K 在 MC 里面建立很多很多的农场,总共
- 农场
比农场 至少多种植了 个单位的作物; - 农场
比农场 至多多种植了 个单位的作物; - 农场
与农场 种植的作物数一样多。
但是,由于小 K 的记忆有些偏差,所以他想要知道存不存在一种情况,使得农场的种植作物数量与他记忆中的所有信息吻合。
输入格式
第一行包括两个整数
接下来
- 如果每行的第一个数是
,接下来有三个整数 ,表示农场 比农场 至少多种植了 个单位的作物; - 如果每行的第一个数是
,接下来有三个整数 ,表示农场 比农场 至多多种植了 个单位的作物; - 如果每行的第一个数是
,接下来有两个整数 ,表示农场 种植的的数量和 一样多。
输出格式
如果存在某种情况与小 K 的记忆吻合,输出 Yes
,否则输出 No
。
提示
对于
这里
这里的不等式分为
-
,可以转化为 。 -
。 -
。
根据不同情况建边,后面还要多增加一个节点(前面讲了,不再赘述)。
CODE(我怎么越来越喜欢用SPFA了,不是死了吗):
#include<bits/stdc++.h>
using namespace std;
struct node{
int v,w;
};
int n,m,u,v,w,op,dis[20010],cnt[20010],vis[20010];
vector<node>g[20010];
queue<int>q;
void spfa(int a){
memset(dis,127,sizeof dis);
queue<int>q;
dis[a]=0,vis[a]=1,q.push(a);
while(!q.empty()){
int u=q.front();
vis[u]=0;
q.pop();
for(int i=0;i<g[u].size();i++){
int v=g[u][i].v,w=g[u][i].w;
if(dis[u]+w<dis[v]){//更新
dis[v]=dis[u]+w;
cnt[v]++;
if(cnt[v]>=n+1){//判环,若有环则不存在
printf("No");
return;
}if(vis[v]==0){//入队
vis[v]=1;
q.push(v);
}
}
}
}printf("Yes");
}int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d%d",&op,&u,&v);
if(op==1){//根据情况建边
scanf("%d",&w);
g[u].push_back((node){v,-w});
}else if(op==2){
scanf("%d",&w);
g[v].push_back((node){u,w});
}else{
g[u].push_back((node){v,0});
g[v].push_back((node){u,0});
}
}for(int i=1;i<=n;i++){//增加节点,讲解如上
g[0].push_back((node){i,0});
}spfa(0);//以增加的节点为源点跑最短路(
return 0;
}
4.最短路计数
最短路计数也是图论中很常见的,实际运用到题目中总会有一些不同。
我们设
-
,说明从源点 到点 的最短路径就是从源点 到点 的最短路径加上边 ,所以 等于 。 -
,说明原来从从源点 到点 的最短路径有 条,现在又可以加上经过点 的这些路径,所以 。
源点
例题
P1144 最短路计数
又是一道绿题
题目描述
给出一个
输入格式
第一行包含
接下来
输出格式
共
提示
对于
对于
对于
模板,我使用的是 SPFA
,在松弛时更新
CODE:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10,mod=1e5+3;
vector<int>g[N];
queue<int>q;
int cnt[N],dis[N],vis[N];
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
g[x].push_back(y),g[y].push_back(x);
}int s=1;
memset(dis,0x3f,sizeof dis);//初始化
dis[s]=0,vis[s]=1,cnt[s]=1;
q.push(s);
while(!q.empty()){
int u=q.front();
q.pop();
vis[u]=0;
for(int i=0;i<g[u].size();i++){
int v=g[u][i];
if(dis[u]+1<dis[v]){//情况1
dis[v]=dis[u]+1,cnt[v]=cnt[u];//松弛时更新
if(vis[v]==0){
q.push(v);
vis[v]=1;
}
}else if(dis[u]+1==dis[v]){//情况2
cnt[v]=(cnt[v]+cnt[u])%mod;//注意取模!
}
}
}for(int i=1;i<=n;i++){
printf("%d\n",cnt[i]);
}return 0;
}
5.传递凸闭包
传递闭包是给出若干的元素以及二元关系,要求这些二元关系存在传递性,通过传递性和二元关系来推出尽量多的元素之间的关系的问题。
注意,这里用到的关系必须是二元关系,若为多元关系,必须要拆解为二元关系。
可以将元素抽象为图的顶点(又tm是抽象),将二元关系抽象为边,这样就可以用 Floyd
求解获得任意两点是否连通的问题(就是问两个元素有没有关系)。
可以将 Floyd
中的转移方程改为
-
要么是直接点
和点 之间有联通关系。 -
要么是点
和点 之间有联通关系,且点 和点 之间有联通关系,可以根据传递性得到点 和点 之间有联通关系。
例题
P2419 [USACO08JAN] Cow Contest S
题目描述
FJ的
输入格式
第一行两个用空格隔开的整数
第
输出格式
输出一行一个整数,表示排名可以确定的奶牛的数目。
提示
样例解释:
编号为
这里如果
其实就是如果第
那么我们就使
那么我们就三重跑一遍前面的状态转移方程
CODE:
#include<bits/stdc++.h>
using namespace std;
int n,m,ret,u,v,cnt[110];
int dis[110][110];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d",&u,&v);
dis[u][v]=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][j]|(dis[i][k]&dis[k][j]);//状态转移方程
}
}
}for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(dis[i][j]==1){
cnt[i]++,cnt[j]++;//可以确定这俩货的关系
}
}
}for(int i=1;i<=n;i++){
if(cnt[i]==n-1){//能确定排名
ret++;
}
}printf("%d",ret);
return 0;
}
由此,最短路的技巧讲完辣!
最小生成树
最小生成树就是从原图中删去任意条边,使得它的边权最小。
kruskal 算法
kruskal 算法是对边操作的。
kruskal 算法要先对边的权值排序,然后枚举所有的边,利用并查集判断会不会形成环,如果不会形成环,将这条边的两端合并一下,然后将记录这条边加入集合,如果统计了
例题
P3366 【模板】最小生成树
题目描述
如题,给出一个无向图,求出最小生成树,如果该图不连通,则输出 orz
。
输入格式
第一行包含两个整数
接下来
输出格式
如果该图连通,则输出一个整数表示最小生成树的各边的长度之和。如果该图不连通则输出 orz
。
提示
数据规模:
对于
对于
对于
对于
这道题就是最小生成树模板,直接给出代码。
CODE:
#include<bits/stdc++.h>
using namespace std;
int n,m,x,cnt,ret,tmp,f[1001000];
struct node{
int u,v,w;
friend bool operator < (node a,node b){
return a.w<b.w;
}
}a[1001000];
int getfa(int x){
if(x==f[x]){
return x;
}return f[x]=getfa(f[x]);
}void merge(int x,int y){
f[getfa(x)]=getfa(y);
}int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
f[i]=i;
}for(int i=1;i<=m;i++){
scanf("%d%d%d",&a[i].u,&a[i].v,&a[i].w);
}sort(a+1,a+m+1);//排序
for(int i=1;i<=m;i++){
if(getfa(a[i].u)!=getfa(a[i].v)){//符合条件
merge(a[i].u,a[i].v);//合并
tmp++;
ret+=a[i].w;
if(tmp==n-1){//得到答案
printf("%d",ret);
return 0;
}
}
}printf("orz");
return 0;
}
咕咕咕
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构