***特别感谢px大佬提供的思路和帮助***
参考博客—1
图论知识总结:(仅仅包含以下几个部分)
1,求最短路的算法和思想:
(1)Floyd-Warshal
(2)Bellman——ford(求负环)
(3)队列优化的Bellman——ford,也就是SPFA(求负环)
(4)Dijkstra(不优化&&优化)
2,求最小生成树的算法,kruskal算法--稀疏图,prim算法——稠密图:
3,求树的直径,两次dfs,两次bfs,dp:
4,扩扑排序:
5,差分约束:
6,求树的割点:
7,求树的割边:
8,例题:
***补充:
(1)稀疏图和稠密图
有很少条边或弧(边的条数|E|远小于|V|²)的图称为稀疏图(sparse graph);
反之边的条数|E|接近|V|²,称为稠密图(dense graph)。
稀疏图一般有邻接表来存储。
(2)
一,求最短路:
1,Froyd——Warshal算法
(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]=min(dis[i][j], dis[i][k] + dis[k][j] );
(2)复杂度
时间复杂度:O(N^3)
空间复杂度:O(N^2)
(3)适合条件:
稠密图和顶点的关系密切
(4)
可以解决负权边;
可以用floyd判断图中哪些点之间有关系(下面附上一道题目链接);
题目链接
题目解析
2,Bellman——ford
//注意,无向图和有向图加边,u-v,只需要加一条b即可。
//处理有向边时,只需要处理u-v的情况即可;
//处理无向边时,只需要处理v-u的情况即可。
(1)算法模板:
注:有向图和无向图,两个顶点之间,只存储一条边的信息。
**有向图:
for(int i=1;i<=n;i++)
{
for(int i=1;i<=n;i++)
book[i]=dis[i];
int k=1;//用来判断负环,求最短路时,可以不用写
for(int j=1;j<=m;j++)
{
if(dis[e[j].v] > dis[e[j].u + e[j].w)
{
dis[e[j].v] = dis[e[j].u + e[j].w;
k=0;
}
}
//如果图没有更新,可以提前退出。
int f=0;
for(int i=1;i<=n;i++)
{
if(dis[i]!=book[i]
f=1;
}
if(f==0)
break;
}
if(k==0) return -1;//表示有负环,这个解释会在后面的例题中说明。
**无向图:
for(int i=1;i<=n;i++)
{
for(int i=1;i<=n;i++)
book[i]=dis[i];
//下面处理无向的时候,对于无向边
//可以考虑加一次边,然后按照下面的情况做;
//也可以考虑加两次边,然后判断一次就行
//但是在有的题中,按照加两条边的方法,不对。
for(int j=1;j<=m;j++)
{
if(dis[e[j].v] > dis[e[j].u] + e[j].w)
dis[e[j].v] = dis[e[j].u] + e[j].w;
//和有向图的区别:(所以说,无向图两点之间也只需要存储一条边)
//要注意:如果题目是有向边和无向边的混合,
//要加一个判断,如果当前是有向边,那么下面的语句就不执行
if(dis[e[j].u] > dis[e[j].v] + e[j].w)
dis[e[j].u] = dis[e[j].v] + e[j].w;
}
//如果图没有更新,可以提前退出。
int f=0;
for(int i=1;i<=n;i++)
{
if(dis[i]!=book[i]
f=1;
}
if(f==0)
break;
}
(2)复杂度
空间:O(M)
时间:O(NM)
(3)适合条件:
稀疏图和边的关系密切
3.SPFA--邻接表存图
(1)算法模板:
//存图(节选重要代码)
struct node
{
int v,w,next;//v,边的终点;w,边的价值;next,和这条边同起点的下一条边。
}edge[num_e];
int head[num_p];//记录以当前节点为起点的最新的一条边
int pre[num_p];//记录路径
int n,cnt;
void int_i()
{
cnt=0;
memset(head,0,sizeof(head));
for(int i=1;i<=n;i++)
pre[i]=i;
return ;
}
void addedge(int a,int b,int c)
{
edge[++cnt].v=b;
edge[cnt].w=c;
edge[cnt].next=head[a];
head[a]=cnt;
return ;
}
//递归输出路径
void print_path(int x)
{
if(x!=pre[x]) print_path(pre[x]);
printf("%d ",x);
return ;
}
int spfa(int s,int t) //s点到t点的距离
{
int dis[num_p];
int book[num_p];//表示哪个点在队列中
int neg[num_p];//判断是否存在负环
int inf=0x3f3f3f3f;
for(int i=1;i<=n;i++)
{
dis[i]=inf;
book[i]=neg[i]=0;
}
dis[s]=0;
queue<int>q;
q.push(s);
book[s]=1;
while(!q.empty())
{
int x=q.front();q.pop();
book[x]=0;
for(int i=head[x];i;i=edge[i].next)
{
int y=edge[i].v;
if(dis[y] > dis[x] + edge[i].w)
{
dis[y] = dis[x] + edge[i].w;
pre[y]=x;
if(book[y]==0)
{
q.push(y);
book[y]=1;
neg[y]++;
if(neg[y] > n) return -1;//出现负圈
}
}
}
}
printf("%d\n",dis[t]);
return 1;
}
int main()
{
int a,b,c;
for(int i=1;i<=m;i++)
{
//输入时存图
scanf("%d%d%d",&a,&b,&c);
addedge(a,b,c);
//如果是无向图:
addedge(b,a,c);
}
spfa(1,n);
}
(2)复杂度:
时间:最坏是:O(NM)
空间:O(M)
(3)可以解决的问题:
稀疏图和边的关系密切,可以解决负权边
4,Dijkstra
(1)(算法模板——邻接矩阵存图)
int e[num_p][num_p];
void dijkstra(int s,int t)//s-->t
{
int dis[num_p];
int book[num_p];
for(int i=1;i<=n;i++)
{
dis[i]=e[s][i];//初始化dis数组
book[i]=0;
}
book[s]=1;
dis[s]=0;
//dijkstra算法核心代码
for(int i=1;i<=n-1;i++)
{
int minn=inf,u;
//找到距离s点没有被使用过的,目前距离s最短的点
for(int j=1;j<=n;j++)
{
if(book[i]==0&&dis[j] < min )
{
min=dis[j];
u=j;
}
}
book[u]=1;
for(int j=1;j<=n;j++)
{
if(e[u][j] < inf)
{
if(dis[j] > dis[u] + e[u][j])
{
dis[j]=dis[u] + e[u][j];
}
}
}
}
printf("%d\n",dis[j]);
return ;
}
int main()
{
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
e[i][j]= i!=j ? inf : 0;
}
int a,b,c;
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&a,&b,&c);
e[a][b]=c;//无向图;
}
}
(2)(算法模板--邻接表存图+优先队列优化)--基于Dijkstra算法的基本思想
//如果看了优化版的Dijkstra,有疑惑,可以先去看上面没有优化的Dijkstra
//二者的算法思想一致
struct node
{
int v,w,next;
node (){};//c++里的构造函数
node(int a,int b)
{
v=a;w=b;
}
bool operator <(const node & s) const//使用了优先队列,对点排序,距离起点近的在前
{
return w > s.w;
}
}edge[num_e];
int head[num_p];//记录以当前节点为起点的最新的一条边
int pre[num_p];//记录路径
int n,cnt;
void int_i()
{
cnt=0;
mesmet(head,0,sizeof(head));
for(int i=1;i<=n;i++)
pre[i]=i;
return ;
}
void addedge(int a,int b,int c)
{
edge[++cnt].v=b;
edge[cnt].w=c;
edge[cnt].next=head[a];
head[a]=cnt;
return ;
}
void dijkstra(int s,int t)
{
int dis[num_p];
int book[num_p];//标记哪个点已经被收缩过了
for(int i=1;i<=n;i++)
{
dis[i]=inf;
book[i]=0;
}
dis[s]=0;
priority_queue<node>q;
q.push(node(s,0));
while(!q.empty())
{
node x=q.top();q.pop();
if(book[x.v])
continue;
book[x.v]=1;
for(int i=head[x.v];i;i=edge[i].next)
{
int y=edge[i].v;
if(dis[y] > x.w + edge[i].w)
{
dis[y]=x.w + edge[i].w;
q.push(node(y,dis[y]));
}
}
}
printf("%d\n",dis[t]);
}
(3)复杂度
未优化的:
时间:O(N^2)
空间:O(M^2)
优化后:
时间:O((M+N)LogN)
空间:O(M)
二,求最小生成树
1,**kruskal算法**
(1)算法模板
struct node
{
int u,v,w;
}edge[num_e];
//并查集
int pre[num_p];
void int_i(void)
{
cnt=0;
for(int i=1;i<=n;i++)
pre[i]=i;
return ;
}
int fa(int x)
{
if(x!=pre[x]) return pre[x]=fa(pre[x]);
return pre[x];
}
int merge(int x,int y)
{
int tx=fa(pre[x]);
int ty=fa(pre[y]);
if(tx!=ty)
{
pre[tx]=ty;
return 1;
}
return 0;
}
int cmp(node a,node b) {
return a.w < b.w;
}
//kruskal算法核心代码
void kruskal(void)
{
sort(edge+1,edge+cnt+1,cmp);
int sum=0,c=0;
for(int i=1;i<=cnt;i++)
{
if(merge(edge[i].u,edge[i].v))
{
sum+=edge[i].w;
c++;
}
if(c==n-1)
break;
}
printf("%d\n",sum);
return ;
}
(2)复杂度分析:
时间:对边的排序,O(E*log2E),并查集的操作O(E),一共O(E*log2(E)+E)
所以可以看出来,如过边很多的话,kruskal算法就很麻烦,所以kruskal算法适合稀疏图,不适合稠密图。
(下面介绍另外一种求最小生成树的代码Prim,适合用于稠密图。)
2,prim算法
(1)算法模板:(来自啊哈c,未优化)
#include
using namespace std;
const int num_p=10010;
const int inf=0x3f3f3f3f;
int n,m;
int e[num_p][num_p],dis[num_p],book[num_p];
void int_i(void)
{
//ÓÃbookÊý×éÀ´±ê¼ÇÄĸöµãÒѾÔÚÊ÷ÖÐÁË¡£
for(int i=1;i<=n;i++)
book[i]=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
e[i][j]= i!=j ? inf : 0;
return ;
}
int main()
{
scanf("%d%d",&n,&m);
int_i();
int a,b,c;
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&a,&b,&c);
e[a][b]=c;
e[b][a]=c;
}
for(int i=1;i<=n;i++)
dis[i]=e[1][i];
//primËã·¨ºËÐÄ£»
int cnt=0,sum=0;
book[1]=1;
++cnt;
while(cnt<n)
{
int minn=inf,u;
for(int i=1;i<=n;i++)
{
if(book[i]==0&&dis[i]<minn)
{
minn=dis[i];
u=i;
}
}
book[u]=1;
cnt++;
sum+=dis[u];
//¸üÐÂδ¼ÓÈëÉú³ÉÊ÷µÄ½Úµãµ½Éú³ÉÊ÷µÄ¾àÀë¡£
for(int i=1;i<=n;i++)
{
if(book[i]==0&&dis[i] > e[u][i])
dis[i]=e[u][i];
}
}
printf("%d\n",sum);
return 0;
}
(2)复杂度:
时间:未优化:O(N^2);优化后的时间复杂度:O(M*log2(N))
(3)对比:(算法的具体选择看题目要求)
Kruskala O(M*log2(M)+M)
Prim(优化) O(M*log2(N))
Prim(未优化) O(N^2)
三,求树的直径:
1,dp数组实现树的直径
//f1 记录从一个点出发的最长单向路径,f2记录从一个点出发第二长单向路径
//f1+f2,就是这个点最大的可以达到的最大长度,枚举所有的点的结果,求最大值即可
//邻接表存图
struct node
{
int v,w,next;
}edge[num_e];
int head[num_p];
int f1[num_p],f2[num_p];
int ans=0,cnt;
void int_i(void)
{
ans=0;
cnt=0;
memset(head,0,sizeof(head));
return ;
}
void addedge(int a,int b,int c)
{
edge[++cnt].v=b;
edge[cnt].w=c;
edge[cnt].next=head[a];
head[a]=cnt;
return ;
}
void dp(int cur,int father)
{
for(int i=head[cur];i;i=edge[i].next)
{
int y=edge[i].v;
if(y==father) //想象以下,有一个点,向四周发散出几条边,有一条边是它刚刚来的边,就是father--》cur的边,这个边要去掉
continue;
dp(y,cur);//继续向下递归
//满足条件的话更新最大值和次大值
if(f1[y] + edge[i].w > f1[cur])
{
f2[cur]=f1[cur];
f1[cur]=f1[y] + edge[i].w;
}
else if(f1[y] + edge[i].w >f2[cur])//只更新次大值
{
f2[cur]=f1[y] + edge[i].w;
}
ans=max(ans,f1[cur] + f2[cur]);
}
return ;
}
2,两次dfs实现树的直径(第一次自己写dfs版本的(还没跑过,我相信它是对的,**嘻嘻**),就写个比较完整的吧,pxgg太强了)
#inlude
using namespace std;
const int num=10000;
struct node
{
int v,w,next;
}edge[num];
int head[num];
//book标记哪些点加入到了直径中
int book[num];
//temp和date用来保存哪些点在树的直径上。
int temp[num];
int date[num];
int cnt,ans,f,n,m;
void int_i(void)
{
cnt=0;
ans=0;
memset(point,0,sizeof(point));
memset(head,0,sizeof(head));
return ;
}
void addedge(int a,int b,int c)
{
edge[++cnt].v=b;
edge[cnt].w=c;
edge[cnt].next=head[a];
head[a]=cnt;
return ;
}
void dfs(int x,int sum,int id)
{
book[x]=1;//标记哪些点被访问过了
temp[id]=x;//记录路径
if(sum>ans)
{
ans=sum;//更新最大值
for(int i=1;i<=id;i++)
date[i]=temp[i];//保存路径
f=x;
}
//以这个点为中心,向四周扩散,递归这些点;
for(int i=head[x];i;i=edge[i].next)
{
if(!book[edge[i].v])
dfs(edge[i].v,sum+edge[i].w,id+1);
}
return ;
}
int main()
{
int x,y,z;
int_i();
while(scanf("%d%d%d",&x,&y,&z)!=EOF)
{
add(x,y,z);
add(y,x,z);
}
ans=0;
memset(book,0,sizeof(book));
dfs(1,0,0);
memset(book,0,sizeof(book));
dfs(f,0,0);
printf("%d\n",ans);
return 0;
}
3,bfs求树的直径(还没掌握,预计这周之前补充)
四,扩扑排序
1,(算法模板)(确定是否有KP有序,不按字典序输出序列)
#define update(a,n) for(int i=1;i<=n;i++) a[i]=0 //取代memset的使用,因为看了一些东西说,memset会增加复杂度!!!!真的
struct node
{
int v,next;
}edge[num_e];
int head[num_p];
int point[num_p];
int cnt,n;
void int_i(void)
{
cnt=0;
update(point,n);
update(head,n);
return ;
}
void add(int a,int b) // a--> b 注意这个顺序要符合题目排序的要求
{
edge[++cnt].v=b;
edge[cnt].next=head[a];
head[a]=cnt;
point[b]++;
return ;
}
void KPsort(void)
{
int ans[n];
int c=0;
int book[n];
update(book,n);
queue<int>q;
for(int i=1;i<=n;i++)
{
if(point[i]==0)
{
q.push(i);
ans[c]=i;
c++:
}
}
while(!q.empty())
{
int k=q.front();q.pop();
for(int i=head[k];i;i=edge[i].next)
{
int y=edge[i].v;
point[y]--;
if(point[y]==0)
{
q.push(y);
ans[c]=y;
c++;
}
}
}
if(c!=n)
printf("存在环\n");
else
{
for(int i=0;i<n;i++)
printf("%d ",ans[i]);
}
return ;
}
五,差分约束
如果对差分约束还不了解,可以先看一下这个博客。
参考博客2
目前差分约束只接触到一道题,不敢说太多,(以后补充)
下面分享一下做的第一道也是唯一一道差分约束的题目。
题目链接
*****总结一下我看到的博客里面的内容:
差分约束的问题可以转化为求最大路径和最小路经的问题。
博客里证明得出,可以将不等式化成:
Xn - X1 <=sum(a1+----+ak);(1)
或者
Xn - X1 >=sum(a1+---+ak); (2)
这两个公式有什么区别呢?
sum(a1+---+ak)是我们可以求的路径
如果要求Xn - X1的**最大值**,就要用公式 (1),然后求出 1和n 之间的**最小路径**;(sum去最小,Xn-X1取最大,二者不就相等;)
如果要求Xn - X1的**最小值**,就要用公式 (2),然后求出 1和n 之间的**最大路径**。
***注意,特别的,如果题目给出的是a+b==c这样的关系,要先把这个公式转化为:
a+b>=c和a+b<=c;***
**题目大体意思:
*N头奶牛,分别有ML个关系,和MD个关系.
*例如:满足ML关系的奶牛a 和 b,之间最大的距离是c,且,a的位置在b的左面,即:b-a<=c;
*a b c.
*满足MD关系的奶牛a 和 b,之间最小的距离是c,且,a的位置在b的左面,即:b-a>=c;
*a b c.
*要求求出(1)这N头奶牛在满足这(ML+MD)种关系时第一头和最后一头奶牛的**最大距离**。(2)如果这个距离可以无限大,则输出-2;
*(3)如果不能满足题目中的关系,输出-1;
&&&解题思路&&&
题目最后让求的是最短路径问题,那么可以将约束关系转化为求最小路径的问题,问题是怎么转化呢??
·将输入的关系转化为边的关系
·因为存在负边,用SPFA算法解决最短路。
·求出上面提出的(1)问题,就是求出最短路径,求出(2)问题,判断二者之间的距离是否为inf;
求出(3)问题,判断是否有负环,如果有负环,又因为Xn - X1 <=sum(``); sum>=0,有负环,求不出最段路径,sum<0。
//基于vector的SPFA。
#include
#include
#include
#include
#include
using namespace std;
const int inf=0x3f3f3f3f;
const int num=1010;
int n,m1,m2;
struct edge
{
int u,v,w;
edge(int a,int b,int c)
{
u=a;v=b;w=c;
}
};
vector<edge>e[num];
int spfa(int s)
{
int dis[num];
bool book[num];
int neg[num];//是否存在负环
for(int i=1;i<=n;i++)
{
dis[i]=inf;
neg[i]=0;
book[i]=false;
}
dis[1]=0;
queue<int>q;
q.push(s);
book[s]=true;
while(!q.empty())
{
int u=q.front();
q.pop();
book[u]=false;
for(int i=0;i<e[u].size();i++)
{
edge x=e[u][i];
if(dis[x.v] >= dis[u] + x.w)
{
dis[x.v]=dis[u]+x.w;
if(book[x.v]==false)
{
book[x.v]=true;
q.push(x.v);
neg[x.v]++;
if(neg[x.v] > n) return -1;
}
}
}
}
if(dis[n]==inf) dis[n]=-2;
return dis[n];
}
int main()
{
scanf("%d%d%d",&n,&m1,&m2);
int a,b,c;
//这些关系表示,b-a<=c;求Xn-X1的最大值,因此把公式转化为 <= 的样子。
for(int i=1;i<=m1;i++)
{
//b-a<=c;b<=a+c;即是,以a为起点,b为终点,c为价值的一条边
scanf("%d%d%d",&a,&b,&c);
e[a].push_back(edge(a,b,c));
}
//把公式转化为 <= 的样子
for(int i=m1+1;i<=m2+m1;i++)
{
//b-a>=c;a-b<=-c;a<=b+(-c);即是以b为起点,a为终点,(-c)为价值的一条边
scanf("%d%d%d",&a,&b,&c);
e[b].push_back(edge(b,a,-c));
}
printf("%d\n",spfa(1));
return 0;
}
六,求树的割点
算法模板(思路来自啊哈c,邻接表优化)
(这里没有这个算法的解释,如果你对啊哈c上的求割点还不了解,建议你,耐心地多看几遍)
//邻接表如何存图,前面算法模板的板块里有。
//第一个数组low[]用来记录点a在不经过某一点b可以达到的最小的祖先的编号。
//num[]用来记录该点的编号;
//flag[]用来标记哪个点是割点。
int low[N],num[N],flag[N];
int index=0;//”时间点“ 第几次到达的。
void dfs(int cur,int father)//cur是当前点,father--》cur的关系
{
int child=0;
num[cur]=++index;
low[cur]=index;
for(int i=head[cur];i;i=edge[i].next)
{
int y=edge[i].v;
if(num[y]==0)//还没有到达过该点
{
child++;
dfs(y,cur);//接着递归下去
low[cur]=min(low[cur],low[y]);
if(cur!=1&&low[y] >= num[cur])
{
flag[cur]=1;
}
if(cur==1&&child==2)
{
flag[cur]=1;
}
}
else if(cur!=father)
{
low[cur]=min(low[cur],num[y]);
}
}
return ;
}
七,求树的割边
***算法模板(算法思路来自啊哈C+邻接表优化)
//下面内容和求割点的算法大部分相同,这里标记的是那条边是割边。
int low[N],num[N],flag[N];
int index=0;
void dfs(int cur,int father)
{
num[cur]=index++;
low[cur]=index;
for(int i=head[cur];i;i=edge[i].next)
{
int x=edge[i].v;
if(num[x]==0)
{
dfs(y,cur);
low[cur]=min(low[cur],low[x]);
//和求割点的不同之处
if(low[y] > now[cur])
{
flag[i]=1;
}
}
else if(x!=father)
{
low[cur]=min(low[cur],low[x]);
}
}
return ;
}
八,例题
Silver Cow Party POJ - 3268