今天是丁明朔老师的讲授~
图论
图是种抽象结构,这种抽象结构可以表示点与点之间的关系。
最短路:
Dijkstra(堆优化)
SPFA
Floyd
最小生成树:
Kruscal
连通性:
BFS / DFS
Tarian(强连通分量)
其他:
拓扑排序
LCA
啥都不说先看下经典例题:
30pts:
我们枚举两个点,搜索它的所有路径,如果所有路径的比值(将路径上所有的传动比相乘)一样的话那就OK,否则就无解;
更好的做法:
图的一个良好的性质:
图的 dfs 树只有返祖边,没有横叉边,如果有横叉边的话,dfs 树的形态就会发生改变;
每个点只到达过一次,且是由其他点到达的,这就符合一棵树的性质;
我们不妨先给所有的齿轮顶一个规矩,让这些作为最初的传动比,我们可以枚举一个点所有的非树边,树边作为一组基础的传动比,非树边来判定传动比是否满足就好了。
绪论LCA
Tarian 的 LCA 的时间复杂度:O(n+m);
最短路
单源最短路算法:
Dijkstra:只能处理正边,时间复杂度稳定;
SPFA:可以处理负边,可以判负环,但是时间复杂度很容易被卡;
这里是手写循环队列,STL里的队列也很快,但是不好查 bug;
这里是最长路代码:
多源最短路:
Floyd:
枚举所有中间结点进行扩展;
最小生成树
图的最小的一颗生成树,叫做最小生成树。
图的最小生成树不一定唯一,有个最重要的性质:最大边权最小。
Kruscal
处理无向图的最小生成树。
我们将图的所有边权排一个序,每次取边权最小的一条边,将边的两个端点连到同一个连通块里。
注意到如果两个端点已经在同一个连通块里了,那么我们再连的话就不是最小生成树了,所以我们就舍弃这条边。那么我们问题就转化为如何判断两个点在一个集合中,怎么合并两个点?考虑用并查集!
拓扑排序
拓扑排序可以判环:我们拓扑排序后发现序列的大小不为 n,说明有环;
每次拿掉一个入度为0的点,将这个点的所有出边全部删掉,这样的话就会产生一大批新的入度为0的点,那么我们将其入栈就好了,直到栈里元素为空。
例题:
最先题目让求最大值最小,那么用二分;,这样本来一个复杂度最大值最小的问题,被我们转化成了简单的判定问题;
我们二分在电话费上花费 mid 元,看是否能从点 1 走到点 n;
接下来我们判定 mid 可行不可行:看能否有小于 k 条边权大于 mid 的边。
我们将全部边权小于 mid 的边的权值设为 0,大于 mid 的边的设为 1,我们跑一遍最短路,得出的答案一定是最小的有大于 mid 的边的条数,如果这个数是小于等于 k 的话(全部将其免费),那就说明这个方案可行,然后我们不断二分直到找到最优解就好了。
直接 Kruscal 就好了,因为最小生成树满足一个性质:最大边权最小。
我们可以考虑在建图方面进行改造:
假设我们建完图之后是长这个样子的:
我们先考虑 k=1 的情况,也就是我们可以使一条边的权值变为0.。
我们可以将这个图在上层拷贝一份,将每个结点从上层的能到达的结点连一条有向边,这些边的权值为0,这样就实现了删除一条边的效果:
那么怎么设置上层点的编号呢?
我们可以像棋盘一样设置:
假设我们有个 n × m 的棋盘,那么这个棋盘上第 k 行第 j 列的数就是:(k-1)* m + j;
那么我们也可以根据这个规律来给上层的图的结点编号:(2-1)* n + j;
那么最终的答案就是:1 -> 6 的最短路,一遍Dijkstra 即可;
分析完上面 k=1 的情况后,我们就可以得出删除 k 条边的做法了:
我们拷贝 k 层图,按照上面的思路连权值为0的有向边,那么最后的答案就是:1 -> (k-1)* n 点的最短路;
我们这样建边实现边的跳跃。
分层图的实现:通过映射,将二维图转化为一维图;
翻译一下题面:
虫洞是单向负边,小路是双向正边,我们看是否能回到过去。
其实就是求图中是否有负环。
我们可用SPFA来判定负环:如果一个结点进入队列的次数超过了 n 次,那就说明图中存在负环;
AC代码:
#include<iostream> #include<cstdio> #include<cstring> #include<queue> using namespace std; const int N=200086; const int inf=1e9; int t,m,n,x,y,w,k,edge_sum; int head[N],vis[N],times[N],dis[N]; struct pos { int from,to,next,dis; }a[N]; queue<int> q; void add(int from,int to,int dis) //链式前向星存图 { edge_sum++; a[edge_sum].next=head[from]; a[edge_sum].from=from; a[edge_sum].to=to; a[edge_sum].dis=dis; head[from]=edge_sum; } int spfa() { for(int i=1;i<=n;i++) { vis[i]=0; //有没有在队列里 dis[i]=inf; //初始化无穷大 times[i]=0; //记录每个点入队次数 } vis[1]=1; //题目要求从标号为1的点开始找负环 dis[1]=0; times[1]++; q.push(1); while(!q.empty()) { int u=q.front(); q.pop(); vis[u]=0; for(int i=head[u];i;i=a[i].next) //枚举u的每条出边 { int v=a[i].to; if(dis[v]>dis[u]+a[i].dis) //松弛操作 { dis[v]=dis[u]+a[i].dis; if(!vis[v]) { q.push(v); vis[v]=1; times[v]++; if(times[v]>=n) return 0; //这个是判负环的条件 } } } } return 1; } inline int read() { int a=0,f=1; char ch=getchar(); while(ch>'9'||ch<'0') { if(ch=='-') f=-f; ch=getchar(); } while(ch<='9'&&ch>='0') a=a*10+ch-'0',ch=getchar(); return a*f; } int main() { t=read(); for(int i=1;i<=t;i++) //t组数据 { n=read(); m=read(); k=read(); edge_sum=0; memset(a,0,sizeof(a)); memset(head,0,sizeof(head)); for(int j=1;j<=m;j++) { x=read(); y=read(); w=read(); add(x,y,w); add(y,x,w); } for(int j=1;j<=k;j++) { x=read(); y=read(); w=read(); add(x,y,-w); } if(spfa()==0) printf("YES\n"); //看有没有负环 else printf("NO\n"); } return 0; }
连边技巧:
我们注意到一个街区里的任意两点之间的距离是相同的,所以我们两两连一条边,每条边的边权相同?这样的话我们一个集合要连 n2 条边,显然不行。
假设我们有个集合:
我们可以在每个集合中设一个虚点,使得集合中的每个点到这个虚点的距离为 ti(集合内任意两点的距离):
然后我们让这个虚点到每个结点的距离为0:
这样建边的话,一个集合只需要建 2n 条边,而能满足集合内任意两点的距离是相等的。(因为都要经过虚点)
我们让每个集合都进行这样的操作后,然后直接 Dijkstra 从 1 -> n 跑一遍最短路即可。
我们最好一开始就在有宝物的位置,这样就不用再动身前往第一个藏有宝物的位置;
我们按照 dfs 序走是最好的,不按 dfs 序走的话可能会走大量的重边;
我们将有宝物的结点按照 dfs 序排序(从小到大),相邻两两求个 LCA,目的是求这两个点的最短路,别忘了求第一个点和最后一个点的最短路(最后还要回去);
那么这个问题我们就已经解决了一半了。
看到题目中宝物是在变换的,那么问题就转化成:在一个序列中,我们每次插入或删除一个数,我们求它的前驱和后继,算一遍最短路,更新答案。
至于求前驱和后继的方法:set,线段树,平衡树;
我们发现Bi,j 范围有些大,其实 300 就够了。
飞飞侠,飞飞侠,我们可以将飞飞侠的每次弹射都看作是依次飞天再降落的过程(方便建图)。
例如,我们花费 A [ i ][ j ] 的费用弹射到 B [ i ][ j ] 的位置可以看成是这样的:
然后我们要降落啊,直接往下连一条权值为0的点就行了(下落不需要费用):
然后我们跑三遍最短路,就好了。
图论全是脑子题QwQ~
强连通分量
被定义在有向图当中。
如果两个点能互相到达,就成这两个点是强连通的;
有一个图,任意两个点之间都是强连通的,那么称其为强连通图;
如果图的一个子图是强连通的,就成这个子图是强连通子图;
一个图的极大强连通子图被称为强连通分量;
如果一个图有强连通分量,那么说明这个图中有环;
为了证明强连通分量很重要,先看一道题目:
如果我们将图中的所有强连通分量都看成一个点的话,那么这个图中一定没有环了;
然后我们分几种情况来讨论:
1.在一个强连通分量里,所有的牛都是互相喜欢的;所以如果我们在强连通分量里找到了一个出度为0的点,那就保证所有的牛都喜欢它,而它不喜欢任何牛,那么就说明不存在有其他的牛是明星;
2.如果我们不只找到了一个出度为0的点,那么说明无法满足所有的牛都喜欢它,则没有明星;
如何求强连通分量?这就要用到了一个算法:Tarjan。
不妨先求个 dfs 树。
(小知识:无向无环图只有返祖边没有横叉边,而有向无环图可以有横叉边。
dfn [ x ]:表示 x 是第几个被 dfs 到的数;
low [ x ]:当前结点以及它的子树的所有出边的所能连到的 dfn 值最小的那个;
scc [ x ]:表示 x 在第几个强连通分量中;
我们使用tarjan的方法 :
(1)、首先初始化 dfn [ u ] = low [ u ] = 第几个被dfsdfs到
(2)、将 u 存入栈中,并将 vis [ u ] 设为 true
(3)、遍历 u 的每一个能到的点,如果这个点 dfn 为0,即仍未访问过,那么就对点 v 进行 dfs,然后 low [ u ] = min(low [ u ] , low [ v ])
(4)、假设我们已经 dfs 完了 u 的所有的子树,那么之后无论我们再怎么 dfs,u点的 low 值已经不会再变了。
至此,tarjan 完美结束。
那么如果 dfn [ u ] = low [ u ] 这说明了什么呢?
再结合一下 dfn 和 low 的定义来看看吧:
dfn 表示 u 点被 dfs 到的时间,low 表示 u 和 u 所有的子树所能到达的点中 dfn 最小的。
这说明了 u 点及 u 点之下的所有子节点没有边是指向 u 的祖先的了,即我们之前说的 u 点与它的子孙节点构成了一个最大的强连通图即强连通分量;
此时我们得到了一个强连通分量,把所有的 u 点以后压入栈中的点和 u 点一并弹出,将它们的 vis 置为 false,如有需要也可以给它们打上相同标记(同一个数字);
void tarjan(int u){ dfn[u]=++ind; low[u]=dfn[u]; //初始化 s[top++]=u; //这个结点入栈 in[u]=1; //在栈里 for(int i=head[u];i;i=e[i].next){ //枚举u的所有出边 int v=e[i].to; if(dfn[v]==0){ //没遍历到,说明v在子树里面 tarjan(v); //搜v low[u]=min(low[u],low[v]); //更新一下u的值 }else{ //如果之前遍历到过了,说明v不在子树里 if(in[v]){ //如果v在栈里面,说明v比u先遍历到,所以该边是返祖边 low[u]=min(low[u],dfn[v]);//注意这里是dfn[v],因为返祖边 } } } if(dfn[u]==low[u]){ //发现了一个强连通分量 cnt_scc++; //强连通分量的个数加一 while(s[top]!=u){ //将u之上的数都弹出作为这个强连通分量里的元素 top--; in[s[top]]=0; scc[s[top]]=cnt_scc; } } }
例一:
按照 g 升序排序,枚举二分一个 g0,将所有 g 小于等于 g0 的边按照 s 求一个最小生成树,然后每次往里加边的时候回形成一个环,任何我们删掉这个环中权值最大的,这样就能维护最小生成树;
例二:
一个定理:平面图的最小割等于其对偶图的最短路。
割:使得起点和终点不连通所删去的边所组成的集合;
最小割:所有的割当中边权总和最小的那个割;
怎么画对偶图?
我们将图中的每个三角形抽象看成一个点(红点):
然后我们在左下角和右上角各设置一个点,我们将与这两个点相邻的边上的所有红点连起来,这个边的权值就是连接红点时穿过的边的权值(红点之间也要玄学得连起来,但是注意要对偶):
我们画出来对偶图之后,这个题就基本解决了,我们自己手动走一遍就可以知道:在对偶图中我们从我们设的这个左下角的点走到右上角的点的任意一条路径,删去路径上所有经过的边,都是一个割!而这条路径上每条边的和就是这个割的值;
那么我们的问题就转化成:在这个对偶图上跑一遍最短路就好了,求出的就是最小割。
例三:
求一个环:这个环的点权之和除以边权之和最大;
我们可以把点权转化成边权上,每个边维护两个信息:Ti,Fi;
我们二分枚举一个 t 作为最终答案,那么∑ Ti / ∑Fi <= t,∑ Ti <= ∑Fi * t,∑ Ti - ∑Fi * t <= 0,∑(Ti - Fi * t) <=0;
所以问题就是转化成:判断图中是否有负环。
这种问题被称为分数规划,做法是二分答案+SPFA判负环。
最优比率生成树
求一个生成树 T ,使得 ∑ Ti / ∑Fi 最大。
解法——0/1分数优化:
我们二分答案 t,那么∑ Ti / ∑Fi <= t,∑ Ti <= ∑Fi * t,∑ Ti - ∑Fi * t <= 0,∑(Ti - Fi * t) <=0;
那么问题就转化成:判断图中是否有负环。
例四:
如果一个强连通分量里面有酒吧,那就将这个强连通分量里的钱全部抢掠,那么我们就可以将这个强连通分量缩成一个点,这个点的值就是强连通分量内的值的总和,我们再跑一遍SPFA就好了。
倍增Floyd
Floyd快速幂
g1 [ i ][ j ]:表示从 i 到 j 只经过一条边的最短路;
我们枚举所有中点 k,g2 [ i ][ j ] = g1 [ i ][ k ] + g1 [ k ][ j ],这个 k 是所有 k 中最小的;
则有:
g3 [ i ][ j ] = g2 [ i ][ k ] + g1 [ k ][ j ],这个 k 是所有 k 中最小的;
g4 [ i ][ j ] = g2 [ i ][ k ] + g2 [ k ][ j ],这个 k 是所有 k 中最小的;
gn [ i ][ j ] = gn/2 [ i ][ k ] + gn/2 [ k ][ j ],这个 k 是所有 k 中最小的;
while(b){ if(b&1){ memset(f,0x3f,sizeof(f)); for(int k=1;k<=n;k++){ for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ f[i][j]=min(f[i][j],ret[i][k]+g[k][j]); } } } memcpy(ret,f,sizeof(f)); } memset(f,0x3f,sizeof(f)); for(int k=1;k<=n;k++){ for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ f[i][j]=min(f[i][j],g[i][k]+g[k][j]); } } } memcpy(g,f,sizeof(f)); } print(ret[S][E])
1.如果两条路径不交叉:
除了最短路上的点,其他全部删掉;
m - dis(s1,t1)- dis(s2,t2);
2.
bfs 求最短路
匈牙利算法
什么是二分图?
我们有两类结点A 和 B,其中只有 A 和 B 之间连边,没有 A 之间或 B 之间连边;
交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边...形成的路径叫交替路。*
增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替路称为增广路。
匈牙利算法的宗旨就是找到一个增广路。
裸的匈牙利算法。
差分约束
差分约束时最小化用最长路;