目录
Bellman-Ford:
欧拉路
概念:从图中的某个点出发遍历整个图,图中的每条边通过且只通过一次
欧拉回路:起点和终点相同的欧拉路
判断欧拉路是否存在:
1)无向连通图:
如果图中的点都是偶点-》存在欧拉回路;任意一点都可以作为起点和终点;如果有二个奇点-〉存在欧拉路;
2)有向连通图:
所有的点的度都是0-》存在欧拉回路;如果存在一个度为1和-1的点其他都是0的图-〉存在欧拉路;
代码实现:
for(int i=1;i<=n;i++){
cin>>u>>v; //输入图,用G来存图
degree[u]++;
degree[v]++; //记录点的度
G[u][v]++; //0不连接 1连接 >1有重边
G[v][u]++;
}
for(int i=1;i<=n;i++){
if(d[i]%2) break; //存在奇点 无欧拉回路
}
输入欧拉回路 代码实现 :
void print(int u){
int v;
for(int v=1;v<=n;v++){ //深度搜索u的所有邻居
if(G[u][v]){
G[u][v]--; //可能有重边
G[v][u]--;
print(v);
cout<<v<<" "<<u;
}
}
}
最短路:
常见算法:Floyd Bellman-Ford SPFA Dijkstra 算法
Floyd算法 :
优点:
1)可以一次求出所有结点之间的最短路径
2)可以处理有负边权的图
思想:运用了动态规划的思想,求i到j 到最短路 ,可以分二种情况,即是否经过图中的某个点k,取二者的最短路径;
判断负边权:
只要在floyd中加入判断是否存在某个group[i][i]<0就行
例题:
/*
*@Author: GuoJinlong
*@Language: C++
*/
//#include <bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<string>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<list>
#include<set>
#include<iomanip>
#include<cstring>
#include<cctype>
#include<cmath>
#include<cstdlib>
#include<ctime>
#include<cassert>
#include<sstream>
#include<algorithm>
using namespace std;
const int mod=1e9+7;
typedef long long ll;
#define ls (rt<<1)
#define rs (rt<<1|1)
#define mid (l+r)/2
#define mms(x, y) memset(x, y, sizeof x)
#define over(i,s,t) for(register long long i=s;i<=t;++i)
#define lver(i,t,s) for(register long long i=t;i>=s;--i)
const int MAXN = 305;
const int INF = 0x3f3f3f3f;
const int N=5e4+7;
const int maxn=1e5+5;
const double EPS=1e-10;
const double Pi=3.1415926535897;
//inline double max(double a,double b){
// return a>b?a:b;
//}
//inline double min(double a,double b){
// return a<b?a:b;
//}
int xd[8] = {0, 1, 0, -1, 1, 1, -1, -1};
int yd[8] = {1, 0, -1, 0, -1, 1, -1, 1};
//start
int G[200][200];
int n,m; //因为floyg是三重循环 所有只能计算很小的图 n<200的情况
void floyd(){
int s=1;
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
if(G[i][k]!=INF) //一个小优化
for(int j=1;j<=n;j++){
if(G[i][j]>G[i][k]+G[k][j]){
G[i][j]=G[i][k]+G[k][j];
}
}
}
}
cout<<G[1][n]<<endl; //1是起点 n是终点 可以写成模版
//cout<<G[s][E]<<endl;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
for(int j=1;j<n;j++){
G[i][j]=INF;
}
}
while (m--) {
int u,v,w;
cin>>u>>v>>w;
G[u][v]=G[v][u]=w;
}
floyd();
return 0;
}
//end
缺点:
因为floyg是三重循环 所有只能计算很小的图 n<200的情况
Bellman-Ford:
功能:用来解决单源最短路径问题 给定一个起点s求它到图中所有n个结点的最短路径
这次我们使用结构体来存边
代码实现:
/*
*@Author: GuoJinlong
*@Language: C++
*/
//#include <bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<string>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<list>
#include<set>
#include<iomanip>
#include<cstring>
#include<cctype>
#include<cmath>
#include<cstdlib>
#include<ctime>
#include<cassert>
#include<sstream>
#include<algorithm>
using namespace std;
const int mod=1e9+7;
typedef long long ll;
#define ls (rt<<1)
#define rs (rt<<1|1)
#define mid (l+r)/2
#define mms(x, y) memset(x, y, sizeof x)
#define over(i,s,t) for(register long long i=s;i<=t;++i)
#define lver(i,t,s) for(register long long i=t;i>=s;--i)
const int MAXN = 305;
const int INF = 0x3f3f3f3f;
const int N=5e4+7;
const int maxn=1e5+5;
const double EPS=1e-10;
const double Pi=3.1415926535897;
//inline double max(double a,double b){
// return a>b?a:b;
//}
//inline double min(double a,double b){
// return a<b?a:b;
//}
int xd[8] = {0, 1, 0, -1, 1, 1, -1, -1};
int yd[8] = {1, 0, -1, 0, -1, 1, -1, 1};
//start
struct edge{
int u,v,w;
}e[1005];
int n,m,cnt;
int pre[1005]; //存放路径
void print_path(int a,int b){ //打印路径 递归实现
if(a==b) cout<<a<<" ";
print_path(a,pre[b]);
cout<<b<<" ";
}
void bellman(){
int s;
s=1;
int d[10010];
mms(d,INF); //初始化为无穷大
d[s]=0;
for(int k=1;k<=n;k++){ //一共n轮操作
for(int i=0;i<cnt;i++){ //检查每条边
int x=e[i].u;
int y=e[i].v;
if (d[x]>d[y]+e[i].w) {
d[x]=d[y]+e[i].w;
pre[x]=y;
}
}
}
cout<<d[n]<<endl;
// print_path(s,n); //如有需要打印路径
}
int main(){
cin>>n>>m;
cnt=0; //记录边数 双向*2
while (m--) {
int a,b,c;
cin>>a>>b>>c;
e[cnt].u=a;
e[cnt].v=b;
e[cnt].w=c;
cnt++;
e[cnt].u=b;
e[cnt].v=a;
e[cnt].w=c;
cnt++;
}
bellman();
}
//end
判断负圈:
当没有负圈时 只需要n轮就可以结束如果超过n轮 最短路还有变化的话 则存在负圈
1)在二个for结束后 检查所有边 如果d(u)>d(v)+w(u,v)这还不是最短存在
2)优化代码如下:
void bellman(){
int s;
s=1;
int d[10010];
mms(d,INF);
d[s]=0;
// for(int k=1;k<=n;k++){
// for(int i=0;i<cnt;i++){
// int x=e[i].u;
// int y=e[i].v;
// if (d[x]>d[y]+e[i].w) {
// d[x]=d[y]+e[i].w;
// pre[x]=y;
// }
// }
// }
int k=0;
bool update=1;
while (update) {
k++;
update=0;
if(k>n) {
cout<<"有负圈"<<endl;
break;
}
for(int i=0;i<cnt;i++){
int x=e[i].u;
int y=e[i].v;
if(d[x]>d[y]+e[i].w){
d[x]=d[y]+e[i].w;
update=1;
}
}
}
cout<<d[n]<<endl;
// print_path(s,n);
}
SPFA:
思路:
SPFA其实是bellman算法的优化 通过queue来实现,核心部分是每轮更新所有结点到s 的最短距离 我们通过邻接表来存图
注意 :
防止图点过于大 我们采用链式前向星来存图
代码实现:
/*
*@Author: GuoJinlong
*@Language: C++
*/
//#include <bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<string>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<list>
#include<set>
#include<iomanip>
#include<cstring>
#include<cctype>
#include<cmath>
#include<cstdlib>
#include<ctime>
#include<cassert>
#include<sstream>
#include<algorithm>
using namespace std;
const int mod=1e9+7;
typedef long long ll;
#define ls (rt<<1)
#define rs (rt<<1|1)
#define mid (l+r)/2
#define mms(x, y) memset(x, y, sizeof x)
#define over(i,s,t) for(register long long i=s;i<=t;++i)
#define lver(i,t,s) for(register long long i=t;i>=s;--i)
const int MAXN = 305;
const int INF = 0x3f3f3f3f;
const int N=5e4+7;
const int maxn=1e5+5;
const double EPS=1e-10;
const double Pi=3.1415926535897;
//inline double max(double a,double b){
// return a>b?a:b;
//}
//inline double min(double a,double b){
// return a<b?a:b;
//}
int xd[8] = {0, 1, 0, -1, 1, 1, -1, -1};
int yd[8] = {1, 0, -1, 0, -1, 1, -1, 1};
//start
const int NUM=1000005;
struct Edge{
int to,next,w;
}edge[NUM];
int n,m,cnt;
int head[NUM];
int dis[NUM];
int vis[NUM];
int Neg[NUM];
int pre[NUM];
void print_path(int x,int y){
//和之前的bellman代码一样
}
void init(){
mms(head,-1); //初始化
for(int i=0;i<NUM;i++){
edge[i].next=-1;
}
cnt=0;
}
void addedge(int u,int v,int w){ //前向星存图
edge[cnt].to=v;
edge[cnt].w=w;
edge[cnt].next=head[u];
head[u]=cnt++;
}
int spfa(int s){
mms(Neg,0); //初始化
mms(dis,INF);
mms(vis,0);
dis[s]=0;
queue<int> q;
q.push(s);
while (!q.empty()) {
int u=q.front();
q.pop();
vis[u]=0; //起点出队
for(int i=head[u];~i;i=edge[i].next){ //~1== i!=-1
int v=edge[i].next;
int w=edge[i].w;
if(dis[u]+w<dis[v]){ //u的第I个邻居v 它借道u 到s更近
dis[v]=w+dis[u]; //更新第I个邻居到s的距离
pre[v]=u; //记录路径
if(!vis[v]){ //v更新了但不在队列
vis[v]=1;
q.push(v);
Neg[v]++;
if(Neg[v]>n) return 1; //出现负圈
}
}
}
}
cout<<dis[n]<<endl;
// print_path(s,n);
return 1;
}
int main(){
int n,m;
cin>>n>>m;
while (m--) {
init();
int a,b,c;
addedge(a,b,c);
addedge(a,b,c);
}
spfa(1);
}
//end
Dijkstra 单源最短路
思想用到了贪心:就是抄近路走,肯定可以找到最短路
方法是通过结构体存图 前向星+优先队列
先附上例题:
武-NC15522(Dijsktra最短路算法)_m0_57006708的博客-CSDN博客
/*
*@Author: GuoJinlong
*@Language: C++
*/
//#include <bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<string>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<list>
#include<set>
#include<iomanip>
#include<cstring>
#include<cctype>
#include<cmath>
#include<cstdlib>
#include<ctime>
#include<cassert>
#include<sstream>
#include<algorithm>
using namespace std;
const int mod=1e9+7;
typedef long long ll;
#define ls (rt<<1)
#define rs (rt<<1|1)
#define mid (l+r)/2
#define mms(x, y) memset(x, y, sizeof x)
#define over(i,s,t) for(register long long i=s;i<=t;++i)
#define lver(i,t,s) for(register long long i=t;i>=s;--i)
const int MAXN = 305;
const int INF = 0x3f3f3f3f;
const int N=5e4+7;
const int maxn=1e5+5;
const double EPS=1e-10;
const double Pi=3.1415926535897;
//inline double max(double a,double b){
// return a>b?a:b;
//}
//inline double min(double a,double b){
// return a<b?a:b;
//}
int xd[8] = {0, 1, 0, -1, 1, 1, -1, -1};
int yd[8] = {1, 0, -1, 0, -1, 1, -1, 1};
//start
const int MAX= 1e5+10;
struct node {
int x,to,next,w;
bool operator <(const node &a) const{
return this->w>a.w;
}
}G[MAX<<1];
int n,k,s;
int cnt;
int dis[MAX];
int vis[MAX];
int head[MAX];
void add(int u,int v,int w){
G[++cnt].to=v;
G[cnt].w=w;
G[cnt].next=head[u];
head[u]=cnt;
}
void dijkstra(int s){
mms(dis,INF);
dis[s]=0;
priority_queue<node> p;
node t;
t.x=s;
t.w=0;
p.push(t);
while (!p.empty()) {
node u=p.top();
p.pop();
int v=u.x;
if(dis[v]!=u.w) continue;
for(int i=head[v];i;i=G[i].next){
int to=G[i].to;
if(dis[to]>dis[v]+G[i].w){
dis[to]=dis[v]+G[i].w;
t.x=to;
t.w=dis[to];
p.push(t);
}
}
}
}
int main(){
cin>>n>>s>>k;
int a,b,c;
for(int i=1;i<n;i++){
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
}
dijkstra(s);
sort(dis+1,dis+1+n);
cout<<dis[k+1];
}
//end
Tarjan算法:
强连通: 在一个有向图G里,如果有两个点(a、b)可以相互到达,我们就叫这两个顶点(a,b)为强连通。
强连通图: 如果在一个有向图G中,每两个点都强连通(可以相互到达),我们就叫这个图为强连通图。
强连通分量(SCC): 在一个有向图G中,有一个子图,这个子图每2个点都满足强连通,我们就叫这个子图叫做 强连通分量,孤立的点也是一个强连通分量。
Tarjan算法
Tarjan算法是一种用来求解有向图强连通分量的线性时间的算法。可以找强连通分量,也可以找缩点、割点等。
算法思路:Tarjan算法是基于DFS的,每个强连通分量为搜索树的一棵子树,搜索时把当前搜索树中未处理的节点加入一个栈,回溯时判断栈顶到栈中节点是否为强连通分量。
为了使这颗搜索树在遇到强连通分量的节点的时候能顺利进行。在DFS时每个点都需要记录两个数组:
DFN[u]: 代表u点DFS到的时间,即时间戳,简单来说就是 第几个被搜索到的。可知在同一个DFS树的子树中,DFN[u] 越小,则其越浅。
Low[u]: 代表在DFS树中,u或u的子树 能够追溯到的最早的栈中节点的 时间戳。
算法过程:
(1)数组的初始化:对图进行深度优先搜索(DFS),在搜索过程中用 DFN 记录搜索的顺序。当首次搜索到点u 时,DFN 与 Low 数组的值都为 到该点的时间。
(2)堆栈:采用栈(记录已经搜索过的但是未删除的点),每搜索到一个点,将它压入栈顶。如果这个点有 出度 就继续往下找,直到找到底。
(3)每次返回时都将子节点与该节点的Low值进行比较,谁小就取谁,保证最小的子树根。当点u 有与点u’ 相连时,如果此时(时间为DFN[u]时)u’不在栈中,u的 Low 值为两点的 Low值 中较小的一个。
当点u 有与点u’ 相连时,如果此时(时间为DFN[u]时)u’在栈中,u的 Low 值为 u的 Low值 和u’的 DFN值 中较小的一个
(4)每当搜索到一个点经过以上操作后(也就是子树已经全部遍历)的 Low值等于DFN值( DFN[u] == Low[u]),则将它以及在它之上的元素弹出栈。这些出栈的元素组成一个强连通分量。原因:u点在DFS树中,子节点(后代)不能找到更浅的点,那么 u点及其后代构成一个SCC(强连通分量)。且 u点 是这个强连通分量的根节点(因为这个Low[] 值是这个强连通分量里最小的。)
(5)继续搜索(或许会更换搜索的起点,因为整个有向图可能分为两个不连通的部分),直到所有点被遍历。
const int MAX=10010;
vector<int>G[MAX];
int dfn[MAX],low[MAX],sc[MAX],s[MAX];//dfn第一次出现的序号 low在dfs树中u或者u的子树可以追溯到最早的时间撮
int cnt,ans,top;
void dfs(int u){
s[top++]=u;
dfn[u]=low[u]=++cnt;
for(int v:G[u]){
if(!dfn[v]){//如果不在栈中uv2点low的最小值
dfs(v);
low[u]=min(low[u],low[v]);
}
else if(!sc[v]){//在栈中low dfn取最小
low[u]=min(low[u],dfn[v]);
}
}
if(low[u]==dfn[u]){
ans++;
while (1) {//将v之前的元素全部推栈
int v=s[--top];
sc[v]=ans;
if(u==v) break;
}
}
}
void Tarjan(int n){
ans=top=cnt=0;
mms(dfn,0);
mms(low,0);
mms(sc,0);
for(int i=1;i<=n;i++){//继续搜索
if(!dfn[i]){
dfs(i);
}
}
}
int main(){
int n,m,u,v;
while (cin>>n>>m&&(n||m)) {
for(int i=1;i<=n;i++) G[i].clear();
for(int i=1;i<=n;i++){
cin>>u>>v;
G[u].push_back(v);
}
Tarjan(n);
ans==1?cout<<"YES"<<endl:cout<<"NO"<<endl;
}
}
最小生成树
一个图中可能存在多条相连的边,我们一定可以从一个图中挑出一些边生成一棵树。这仅仅是生成一棵树,还未满足最小,当图中每条边都存在权重时,这时候我们从图中生成一棵树(n - 1 条边)时,生成这棵树的总代价就是每条边的权重相加之和。
prim (普里姆算法)
思路:
Prim算法每次循环都将一个蓝点u变为白点,并且此蓝点u与白点相连的最小边权min[u]还是当前所有蓝点中最小的。这样相当于向生成树中添加了n-1次最小的边,最后得到的一定是最小生成树。
例题:
代码实现:
const int MAX=1010;
int vis[MAX];
int dis[MAX];
int n,m;
ll sum;
int a[MAX][MAX];
ll prim(int pos){
dis[pos]=0;
for(int i=1;i<=n;i++){
int cur=-1;
for(int j=1;j<=n;j++){
if(!vis[j]&&(cur==-1||dis[j]<dis[cur])){
cur=j;
}
}
if(dis[cur]>=INF) return INF;
sum+=dis[cur];
vis[cur]=1;
for(int k=1;k<=n;k++){
if(!vis[k]){
dis[k]=min(dis[k],dis[cur]+a[cur][k]);
}
}
}
return sum;
}
int main(){
cin>>n>>m;
mms(dis,INF);
mms(a,INF);
for(int i=0;i<m;i++){
int u,v,w;
cin>>u>>v>>w;
a[u][v]=w;
a[v][u]=w;
}
cout<<prim(1)<<endl;
}
kruskal (克鲁斯卡尔算法)
Kruskal算法将一个连通块当做一个集合。Kruskal首先将所有的边按从小到大顺序排序(一般使用快排),并认为每一个点都是孤立的,分属于n个独立的集合。然后按顺序枚举每一条边。如果这条边连接着两个不同的集合,那么就把这条边加入最小生成树,这两个不同的集合就合并成了一个集合;如果这条边连接的两个点属于同一集合,就跳过。直到选取了n-1条边为止。
const int MAX=100010;
struct node{
int u,v,w;
}e[MAX];
bool cmp(node a,node b){
return a.w<b.w;
}
int n,m;
int fa[MAX];
int find(int x){
if(x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
ll sum;
int main(){
cin>>n>>m;
for(int i=0;i<=n;i++)
fa[i]=i;
for(int i=1;i<=m;i++){
cin>>e[i].u>>e[i].v>>e[i].w;
}
sort(e+1,e+1+n,cmp);
for(int i=1;i<=m;i++){
int x=find(e[i].u);
int y=find(e[i].v);
if(x==y) continue;
fa[y]=x;
sum+=e[i].w;
}
int ans=0;
for(int i=1;i<=n;i++){
if(i==fa[i])
ans++;
}
if(ans>1){
cout<<"NO"<<endl;
}
else {
cout<<sum<<endl;
}
}