图论
Tags:Noip前的大抱佛脚
知识点
二分图相关
DFS找环
From [CodeForces19E] Fairy
环一定是DFS树上的返祖边
通过对偶环+1,奇环-1可以找到出现在所有奇环上的一条边
这种方法也可以求图中最多的不相交的圆的个数
并查集维护二分图
在数据结构有讲,用按秩合并的带权并查集维护黑白颜色即可
二分图匹配的不可行边
求一定不会出现在二分图最大匹配中的边
方法是:跑网络流,对残余网络跑(Tarjan)缩点,一条边不是不可行边当且仅当它是匹配边或者他们被缩在同一个点中
感性理解:缩在同一点,把环给反向,于是就可以变成匹配边了
注意:Dinic跑二分图复杂度为(O(sqrt n m))!!
最小生成树相关
最短路树
要求生成树中每个点到1号点的距离等于图中最短路,且生成树边权之和最小
这个直接用SPFA或者Dijkstra求从哪里来是最短路,然后贪心选个最短的就好了
最短路相关
负环
可以使用SPFA,先同时把所有点入队,然后判断一个点的最短路如果经过超过(n)个点,则表示存在负环
多源最短路
把源点同时入队/入堆,然后照样跑即可,同时如果需要可以记一个from表示从哪个源点过来的
如果不需要记录,新建源点向所有源点连0的边即可
差分约束系统
连边以及跑最长/短路
对于一些形如 (v_ale v_b+W) 的若干关于"(le)“的式子,可以从从(b)向(a)连一条权值为(W)的边,然后跑最短路
最后跑出来的答案(dis[a])表示的是关于(v_a)的若干不等式的解,就是说不等式组中最小的那个,所以(dis[a])是(v_a)的最大值
如果是要求最小值,那么对于上面的式子,可以从(a)向(b)连(-W)的边,跑最长路,得出的(dis[b])是(b)的最小值
无解以及做题方法
可以通过存在正/负环判断无解
很多时候模型并不明确,可以二分答案之后判断合法性来求解一般问题
01最短路
边权只有0或1的最短路,感觉是Dijkstra+SPFA,操作是开一个双端队列deque,每次更新最短路,如果由+0更新就丢队头,+1更新就丢队尾,然后每个点只往外面更新一次,其实是用队列模拟Dij的堆,这样复杂度是(O(m))的
k短路
-
求一张有向图中(1)到(n)中第(k)短的路径,可以不是简单路径(有环,自环,重边)
做法1:A*搜索
建出以(n)为根的最短路树,那么每个点有两个参数:(f[i])表示到达(i)点已经走了(f[i])的长度,(h[i])表示(i)的估价函数,表示(i)到(n)最少还要走多远,以这个为权值每次在堆中选取权值最小的更新,(n)被走到的第(k)次即为(k)短路
做法2:可持久化左偏树
建出最短路树后我们发现对于任意一条路径,可以拆成树边和非树边,且一个非树边的边集对应着一条路径,于是我们需要求出第(k)小的非树边集
令(dis[i])表示(i)到(n)的最短路长度,则定义一个非树边集(S)的权值为(dis[1]+sum_{ein S}d[e],d[e]=dis[to]+e.w-dis[fr]),(d[e])的实际含义是走这条边新增的代价,于是把这个丢进优先队列里取出(k)次就好了
然后我们考虑怎么得到非树边集&怎么拓展新状态。
对于每个点,维护该点到树根(n)的路径的所有非树边的出边集,并按照出边的(d[e])排序(这个用可持久化左偏树得到,每次从父亲那里copy下来,代码见魔法猪学院)
定义一个全局的优先队列按照(bs+val)排序,(bs)表示已经走了的边的权值,(val)表示当前边集的权值。首先把1号点的左偏树树顶丢入全局堆中,然后从堆中取出k次元素得到第k小
对于一个状态是当前最小,由此可以拓展出三个新状态:该点的左偏树中的左右儿子(表示为从该点向上不选择原来的非树边,转而选择一条比它大一点的非树边。需要堆中父亲一定大于左右儿子的性质做保证)、该条边指向的点的堆顶(表示为一定选这条边,在往n走的路径中再选一条非树边,此时要给(bs_{new}+=bs_{old}+val_{old}))
复杂度:(O(nlogn+mlogm+klogk)),分别为最短路、左偏树、优先队列的复杂度之和
网络流
zkw费用流
用SPFA跑出最短路之后,用类似Dinic的dfs跑很多遍,对于相同费用的路径较多的图的效率特别高,当然不满足这种性质效率就会比一般的费用流效率低(无限之环加上zkw费用流快了100倍!)
int dfs(int x,int flow)
{
if(x==T) return flow;vis[x]=1;
for(int &i=cur[x];i;i=a[i].next)
{
int R=a[i].to;//dis表示的费用最短路
if(!a[i].w||dis[R]!=dis[x]+a[i].cost||vis[R]) continue;
int k=dfs(R,min(flow,a[i].w));
if(k) {a[i].w-=k,a[i^1].w+=k;vis[x]=0;return k;}
}
return 0;//如果在这个点找不到增广路,vis就只会在SPFA中清空,这个点在本次dis数组时不会再访问
}
while(SPFA())
{
for(int i=1;i<=T;i++) cur[i]=head[i];
while(int tmp=dfs(S,inf)) mxfl-=2*tmp,ans+=tmp*dis[T];
}
做题经验
同余类最短路
来源:墨墨的等式
求(a_1x_1+a_2x_2+...+a_nx_n=B),其中(x)都有非负整数解,求(B)在一定范围内的取值个数
巧妙地把每个解归类为(\%a_1)的余数那个点,对每个点往外连边跑最短路,表示得到(\%a_1=k)的最小的(B)是多大,从而推出(B)的个数
边权是max的形式
代价是进入该点和从该点出去的边权(max),求(1)到(n)的最小代价
注意题目是无向图,所以可以每条边拆成两个,然后左右对应,差分建图
其实遇到max都应该要想想差分
图论模型的转换
遇到以下情况可以考虑图论模型来连边
- 有两类点,两类点之间有关联,然后可以考虑把A类点像B类点的两条边转为B类点的一条边(ZKJ太阳月亮匹配/棋盘上的守卫)
- 两个点相互关联,知道其中一个就可以推导另一个
- 一个物品有两个权值,权值值域一定(ZKJ烟火左右权可翻转,求最大权连续序列)
边定向
这是一种思考方向,可以参考[BZOJ4883]棋盘上的守卫
树上点覆盖
一个关键点能够覆盖距离它不超过k的点,要求覆盖所有点的最小关键点数
这题是贪心不是DP啊QAQ
两树叠图的最小割及方案数
答案为2或者3,所以一定是A树断一边,B树断若干边构成的
把A作为树形结构,B树的每条边(tag[x]++,tag[y]++,tag[lca(x,y)]-=2)
所以子树之和便是子树内向外面连的边数之和
一类BFS最小生成树做法
给定网格图/树结构,求一些关键点的最小生成树(10.15YLT3)
方式是以关键点为源跑多源最短路,一个点被经过的第二次/一条边连接的两点的最近源点不同,就可以连接这两个源点了。注意边数还是原来的边数级别的
图论模板库
Tarjan相关
强连通分量:有向图中任意两个顶点都有相互到达的路径的一个极大子图
边双连通分量:一个子图中删去任意一条边都不影响图的连通性
点双连通分量:一个子图中删去任意一个点都不影响图的连通性
割边:连接两个边双的边
割点:连接两个点双的点
割边
//把一个边双缩点
void Tarjan(int x)
{
vis[x]=1;sta[++top]=x;
dfn[x]=low[x]=++tot;
for(int i=head[x];i;i=a[i].next)
{
int R=a[i].to;
if(!dfn[R]) Tarjan(R),low[x]=min(low[x],low[R]);
else if(vis[R]) low[x]=min(low[x],low[R]);
}
if(low[x]!=dfn[x]) return;
for(int k=sta[top],lst=0;lst!=x;lst=k,k=sta[--top])
vis[k]=0,bel[k]=x;
}
//无向图缩点略有不同
割点
//求割点(tag[x]=1)
void Tarjan(int x,int f)
{
int s=0;dfn[x]=low[x]=++tot;
for(int i=head[x];i;i=a[i].next)
{
int R=a[i].to;if(R==f) continue;
if(!dfn[R])
{
s++;Tarjan(R,x);tag[x]|=low[R]>=dfn[x];
low[x]=min(low[x],low[R]);
}
else low[x]=min(low[x],dfn[R]);//注意!这里必须是dfn,有反例!!
}
if(!f&&s==1) tag[x]=0;
}
有向图缩点后成为DAG,无向图缩点/求割点后成为树的结构
圆方树
int n,m,dfn[N],low[N],sta[N],top,node,tot,siz[N];
void Min(int &a,int b) {if(b<a) a=b;}
void Tarjan(int x)
{
dfn[x]=low[x]=++tot;sta[++top]=x;siz[x]=-1;
for(int i=A.head[x];i;i=A.a[i].next)
{
int R=A.a[i].to;
if(dfn[R]) {Min(low[x],dfn[R]);continue;}
Tarjan(R);Min(low[x],low[R]);
if(low[R]>=dfn[x])
{
B.link(++node,x);siz[node]=1;
for(int k=sta[top],lst=0;lst!=x;lst=k,k=sta[--top])
B.link(node,k),siz[node]++;
}
}
//node初值为n
}
2-SAT
具体作用。。。不可描述。。。
大概就是给出每个点选或不选使得满足所有条件吧。
输出方案的话选择超级点编号小的那个选择输出
struct edge{int next,to;}a[N];
int n,m,low[N],dfn[N],tot,sta[N],top,bel[N],node,in[N],head[N],cnt;
int rev(int x) {return x>n?x-n:x+n;}
void Min(int &a,int b) {if(b<a) a=b;}
void link(int x,int y) {a[++cnt]=(edge){head[x],y};head[x]=cnt;}
void Tarjan(int x)
{
dfn[x]=low[x]=++tot;sta[++top]=x;in[x]=1;
for(int i=head[x];i;i=a[i].next)
{
int R=a[i].to;
if(!dfn[R]) Tarjan(R),Min(low[x],low[R]);
else if(in[R]) Min(low[x],low[R]);
}
if(low[x]!=dfn[x]) return;node++;
for(int lst=0,k=sta[top];lst!=x;lst=k,k=sta[--top])
bel[k]=node,in[k]=0;
}
int main()
{
cin>>n>>m;
for(int i=1,x,a,y,b;i<=m;i++)
{
scanf("%d%d%d%d",&x,&a,&y,&b);
x+=a*n;y+=b*n;
link(rev(x),y);link(rev(y),x);
}
for(int i=1;i<=n*2;i++) if(!dfn[i]) Tarjan(i);
for(int i=1;i<=n;i++)
if(bel[i]==bel[rev(i)]) return puts("IMPOSSIBLE"),0;
puts("POSSIBLE");
for(int i=1;i<=n;i++)
printf("%d ",bel[i]<bel[rev(i)]?0:1);
}
TarjanLCA
(O(n*反阿克曼函数+Q))离线求lca
步骤:
- 对询问挂链(vector),初始化并查集
- (dfs)整棵树后处理询问,扫完该子树才将其并查集父亲指向树结构的父亲
- 如果对于一个询问((x,y)),当前扫到(y)且(x)已经被访问过,答案为x的并查集父亲
void tarjan(int x,int fr)
{
for(int i=head[x];i;i=a[i].next)
if(R!=fr) tarjan(R,x);vis[x]=1;
for(int i:U[x])
{
int y=g[i]^x,&s=ans[i];//g[i]=x^y,表示第i个询问
if(vis[y]) s=find(y);
}
fa[x]=fa[fr];
}
最短路相关
SPFA判负环
方式是先把所有点入队了,然后若有一个点的最短路超过n个点即存在负环
复杂度(O(n^2))
注意判自环!
for(int i=1;i<=n;i++) Q.push(i),vis[i]=1;
while(!Q.empty())
{
int x=Q.front();
for(int i=head[x];i;i=a[i].next)
{
int R=a[i].to;
if(dis[R]<=dis[x]+a[i].w) continue;
dis[R]=dis[x]+a[i].w;
f[R]=f[x]+1;
if(f[R]==n+1) return 1;
if(!vis[R]) Q.push(R),vis[R]=1;
}
Q.pop();vis[x]=0;
}
Dijkstra
稳定的(O(nlogn))单源最短路做法
struct Node
{
int x;ll dis;
int operator < (const Node&A)const
{return dis>A.dis;}
};
int n,m,vis[N],head[N],cnt,S;ll dis[N];
priority_queue<Node> Q;
void Dijkstra()
{
memset(dis,127,sizeof(dis));
Q.push((Node){S,0});dis[S]=0;
while(!Q.empty())
{
int x=Q.top().x;Q.pop();
if(vis[x]) continue;vis[x]=1;
for(int i=head[x];i;i=a[i].next)
{
int R=a[i].to;
if(dis[R]<=dis[x]+a[i].w) continue;
dis[R]=dis[x]+a[i].w;
Q.push((Node){R,dis[R]});
}
}
}
网络流
最大流
(Dinic)算法(O(n^3)),二分图(O(msqrt n))
const int N=1e4+10,inf=1e9;
struct edge{int next,to,w;}a[N*21];
int n,m,S,T,dep[N],head[N],cnt=1,cur[N],ans;
queue<int> Q;
void link(int x,int y,int w)
{
a[++cnt]=(edge){head[x],y,w};head[x]=cnt;
a[++cnt]=(edge){head[y],x,0};head[y]=cnt;
}
int BFS()
{
memset(dep,0,sizeof(dep));
Q.push(S);dep[S]=1;
while(!Q.empty())
{
int x=Q.front();Q.pop();
for(int i=head[x];i;i=a[i].next)
if(!dep[a[i].to]&&a[i].w) dep[a[i].to]=dep[x]+1,Q.push(a[i].to);
}
return dep[T];
}
int DFS(int x,int flow)
{
if(x==T) return flow;
for(int &i=cur[x];i;i=a[i].next)
{
int R=a[i].to;
if(!a[i].w||dep[R]!=dep[x]+1) continue;
int k=DFS(R,min(flow,a[i].w));
if(k) {a[i].w-=k;a[i^1].w+=k;return k;}
}
return 0;
}
int main()
{
cin>>n>>m>>S>>T;
for(int i=1,x,y,w;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&w);
link(x,y,w);
}
while(BFS())
{
for(int i=1;i<=n;i++) cur[i]=head[i];
while(int tmp=DFS(S,inf)) ans+=tmp;
}
cout<<ans<<endl;
return 0;
}
费用流
struct edge{int next,to,w,c;}a[N*20];
int n,m,S,T,head[N],cnt=1,dis[N],pe[N],px[N],vis[N];
ll ans,f;queue<int> Q;
void link(int x,int y,int w,int c)
{
a[++cnt]=(edge){head[x],y,w,c};head[x]=cnt;
a[++cnt]=(edge){head[y],x,0,-c};head[y]=cnt;
}
int SPFA()
{
memset(dis,63,sizeof(dis));
Q.push(S);dis[S]=0;vis[S]=1;
while(!Q.empty())
{
int x=Q.front();
for(int i=head[x];i;i=a[i].next)
{
int R=a[i].to;
if(dis[R]<=dis[x]+a[i].c||!a[i].w) continue;
dis[R]=dis[x]+a[i].c;
pe[R]=i;px[R]=x;
if(!vis[R]) vis[R]=1,Q.push(R);
}
Q.pop();vis[x]=0;
}
return dis[T]!=dis[0];
}
int main()
{
cin>>n>>m>>S>>T;
for(int i=1,x,y,w,f;i<=m;i++)
{
scanf("%d%d%d%d",&x,&y,&w,&f);
link(x,y,w,f);
}
while(SPFA())
{
int flow=1e9;
for(int i=T;i!=S;i=px[i]) flow=min(flow,a[pe[i]].w);
for(int i=T;i!=S;i=px[i]) a[pe[i]].w-=flow,a[pe[i]^1].w+=flow;
f+=flow,ans+=flow*dis[T];
}
cout<<f<<" "<<ans<<endl;
}