【清北】【笔记】图论

欧拉回路

经过整个图的所有边的路径。

连通图

令无向图G=(V,E),如果∀x,y∈V,x和y之间都存在路径。

强连通图

令有向图G=(V,E),如果∀x,y∈V,x到y都存在路径。

图的遍历

有2种最简单的方法:
深度优先搜索(DFS)和广度优先搜索(BFS)
前者的原则是建立一个栈,只要栈顶结点u还有相邻的点v未入过栈,就把v入栈,遍历v,继续递归地搜索,当栈顶结点u的相邻结点都入过栈时,将u出栈。
后者则是建立一个队列,每次从队头取出一个结点,然后将相邻的没进过队列的结点入队并遍历,然后再取出一个结点如此做。

最短路径问题

性质:
给定s—>t 不存在负权:最短路一定是简单路径
从s—>t最短路,走到v0时一定继承了前面的最短路
每个节点的最短路可以组织成一棵树

  • Dijkstra算法

这算法的时间主要耗费在两个地方:松弛结点,找最小值。
对于找最小值的部分,如果直接枚举的话,对于固定完所有结点才能结束的最坏情况,时间复杂度可达到O(|V|2),此时松弛操作的时间复杂度为O(|E|)。总的时间复杂度为O(|V|2+|E|)。

如果我们用一个高效支持单元素修改,询问全体最小值的数据结构来记录(线段树/堆/优先队列),那么在单次修改复杂度变为O(log|V|)的前提下,单次询问的时间复杂度为O(1)。因此总的时间复杂度为O(|E|log|V|+|V|)。

用pair的原因:自带比较函数(偷懒)
加堆优化的程序实现(使用邻接链表,优先队列):

//pair存路径长度和节点
priority_queue<pair<int,int>> que; while (!que.empty()) que.pop();
for (i=1;i<=n;i++) dist[i]=+inf; dist[s]=0; que.push(make_pair(0,s));
while (!que.empty()) {
    int nd=que.top().second; que.pop();
    if (closed[nd]) continue; closed[nd]=1;
    for (p=fir[nd];p!=0;p=p->next)
        if (dist[p->to]>dist[nd]+p->w) {
            dist[p->to]=dist[nd]+p->w;
            que.push( make_pair(-dist[p->to],p->to) );
        }
}

Dijkstra算法的局限性
经过堆优化的dijkstra算法可以应付|V|和|E|比较大的情况,但是这个算法无法处理负权边,换句话说,这个算法的贪心的依据在图中有负权边的时候不成立。因此在有负权边的时候,我们需要换用别的方法。

  • Bellman-ford/spfa算法

我们可以采取枚举的方案。
用一个数组d[ ]记录从s出发到所有结点当前的最短路径。
一开始设d[s]=0,然后枚举所有的边,利用三角形不等式更新最短距离。
不断枚举直到某一次枚举中没有结点被更新。
如果没有负权回路,算法应当在第|V|次枚举之前就求出所有结点的最短路径。
如此做的时间复杂度为O(nm)。
耗时巨大:用队列优化—–>spfa。

//链表
struct Edge{ int to; int dist; Edge* next; } edge[max_Esize],*fir[max_Vsize]; int edges;
for (i=1;i<=n;i++) dist[i]=+inf, inq[i]=0; //1为起点,先假设不存在路径
q[0]=1; dist[1]=0; inq[1]=1;    //inq表示该结点是否在队列中
for (h=0;h<t;h++) {     //在n较大的时候宜改成循环队列
    u=q[h]; inq[u]++;       //inq为奇数表示在队内,偶数表示在队外。用奇数和偶数:不仅可以知道是否在队列,还可以知道进了多少次队列。
    for (p=fir[u];p!=0;p=p->next)
        if (dist[v=p->to]>dist[u]+p->dist) {
            dist[v]=dist[u]+p->dist;
            back[v]=u;
            if (!(inq[v]&1)) { inq[v]++; q[t++]=v; }
        }
}

时间复杂度O(k(n+m)),其中k和最短路经过的路径条数有关。
如果图中存在负权回路,前面的程序会死循环。
如果图中不存在负权回路,那么任何的最短路都会是简单路径。而一个阶为n的图,到一个点的最短路所经过的路径数量最多为n-1。如果出现负权回路,那么和负权回路相关的结点必定会无限次经过,只要有一个点入队超过n次,就可以判断出现了负权回路

  • Floyd

这个算法需要依次添加|V|个中间点,每次需要比较|V|2个结点对,因此时间复杂度为O(|V|3)。

最小生成树

  • Kruskal算法

首先证明,整个图G权值最小的边一定在最小生成树里面。
我们在将权值最小的边加入了最小生成树以后,可以将这条边所连接的两个点合成一个点考虑,然后再找下一个权值最小的连接两个不同点的边。
以此类推,我们可以把所有的边按照边权排序,先插入边权较小的边,当某条边插入时两端已经在同一个连通块,就舍弃这条边,否则就插入这条边并合并对应的连通块。
如何判断两个点所在的连通块是否相等?并查集。
时间复杂度:排序O(|E|log|E|),并查集维护O(|E|)
程序实现(用struct Edge{ int u,v,weight; }表示边)

sort(edge,edge+m,cmp); // 按边权从小到大排序
for (i=1;i<=n;i++) fa[i]=i,rk[i]=0;
for (i=0;i<m;i++)
{
    tu=top(edge[i].u);
    tv=top(edge[i].v);
    if (tu==tv) continue;
    if (rk[tu]<rk[tv]) swap(tu,tv);
    if (rk[tu]==rk[tv]) rk[tu]++;
    fa[tv]=tu; ans+=weight;
}
  • Prim算法

首先证明,对于某个结点来说,以其为端点的边当中,权值最小的一条边一定在最小生成树中。(当权值最小的有多条的时候,每一条都存在某棵最小生成树包含之)
也就是说,我们可以从一个结点出发,在相邻的边当中选择一条权值最小的,加入最小生成树,然后将整个连通块当成一个结点,对外再选下一条权值最小的边。
以此类推,我们可以仿照dijkstra中对结点最短距离的维护,只是我们这次维护的是当前连通块连到这个结点的边中权值最小的一条。
不使用堆优化,时间复杂度O(|V|2)
使用堆优化,时间复杂度O(|E|log|V|+|V|)
Prim算法加堆优化:

priority_queue<pair<int,int>> que; while (!que.empty()) que.pop();
for (i=1;i<=n;i++) dist[i]=+inf; dist[s]=0; que.push(make_pair(0,s));
while (!que.empty()) {
    int nd=que.top().second; que.pop();
    if (closed[nd]) continue; closed[nd]=1; ans+=dist[nd];
    for (p=fir[nd];p!=0;p=p->next)
        if (dist[p->to]>p->w) {
            dist[p->to]=p->w;
            que.push( make_pair(-dist[p->to],p->to) );
        }
}

拓扑排序

检查入度法
如果一个活动入度为0,那就表示这个活动没有前置活动,可以放在序列的最前面。将这个活动放入拓扑序列中,并且将其出度全部删除,以找到一个新的入度为0 的结点。
检查入度法的时间复杂度O(n+m)
深度搜索法
我们从任意的结点出发,进行深度搜索,找任意一个vis值<2的相邻结点递归下去,如果我们找到了一个vis值=1的结点,就表示我们找到一个环。
当某个结点不存在相邻结点,或者相邻结点全部vis值为2的时候,这个结点为拓扑序列的最后一位,然后返回。
如果出发的结点返回后仍未加入所有结点,找一个未加入的结点进行上面的操作,直到发现环或者加入所有结点为止。时间复杂度O(n+m)。

检查入度法 //通过邻接链表组织起来的边edge[1..m]

for (i=1;i<=m;i++) rd[edge[i].to]++;
t=0; for (i=1;i<=n;i++) if (rd[i]==0) que[t++]=i;
for (h=0;h<t;h++) { //que表示拓扑顺序
    u=que[h]; 
    for (p=fir[u];p!=0;p=p->next) {
        rd[p->to]--;
        if (rd[p->to]==0) que[t++]=p->to;
    }
}

深度搜索法

bool dfs(int nd)//vis=1被访问在站里 2已返回 0未访问
{
    vis[nd]=1;
    for(each v adjacent to nd)
    {
        if(vis[v]==1) return 0;
        if(vis[v]==0) if(!dfs(v)) return 0;
    }
    vid[nd]=2;
    que[t++]=nd;
    return true;
} 
for(i=1;i<=n;i++)
        if(vis[nd]==0) if(!dfs(i)) break;

例题1
给出一个只含有负权边的图G(|V|<2000000,|E|<3000000),求所有以任意顶点为起点的最短路径长度的最小值。
解答
这一题用前面提到的最短路径一定会超时。
首先如果这图有环,那么就存在负权回路,答案为-∞。
否则可以对这图进行拓扑排序,用dist[v]表示以任意起点以结点v结尾的最短路长度。
然后在进行了拓扑排序的前提下,我们发现,松弛操作只可能是前面的结点对后面的结点进行松弛。所以我们按照这个序列的先后顺序,一个个往后松弛,最后得到以所有结点结尾的最短路长度。
时间复杂度O(|V|+|E|)

例题2
给出一个有向无环图(|V|<2000000,|E|<3000000),顶点v处有kv元钱,现在,任意选定出发的顶点,并在任意终点结束,求最多能从该图收集到多少钱?
解答
首先对这一题进行拓扑排序,按照所得序列的顺序,计算到达每个顶点时能收集到的最多的钱。
用dp[v]表示走到v最多能够得多少钱,然后我们列出状态转移方程:
dp[v]=min(dp[v],dp[u]+k[v])
时间复杂度O(|V|+|E|)

连通/强连通/双连通

无向图G=(V,E)是连通的,当且仅当其中任意两个结点能互相到达,如果G是有向图,这种情况称G是强连通的。

对于一个连通图G来说,如果删掉了边(u,v)以后,会使得图不再连通,那么称这条边(u,v)为桥(也称割边)
如果一个连通图不包含桥的话,就意味着这个连通图无法仅靠删除一条边变为不连通图,此时称这个连通图为双连通图。

连通分量/强连通分量/双连通分量

对于图G的子图G’来说,如果G’连通,那么称G’为G的连通子图。

如果图G的某个连通子图G’的顶集不是其余连通子图G’’的点集的真子集,那么称G’为G的一个连通分量。

对于强连通图和双连通图,也有类似的性质。

如何通过一个无向图求它的连通分量?
简单,bfs
那么如何求出强连通分量呢?
emmmmmm
从每个点出发bfs?O(n2)
借助dfs?tarjan算法?

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值