【学习笔记】关于图论的一些
存图
邻接表
int edgeid,head[N];
struct edge{int v,nxt,w;}e[2*M];
邻接矩阵
int g[N][N];
边集
struct edge{int u,v,w;}e[M];
最小生成树
Kruskal
时间复杂度\(O(m\log{m})\)
算法流程
1.边集存图
2.按权值排序
3.贪心拿权值最小的边,并查集保证无环
注意
1.用并查集一定一定要记得初始化
2.图可能不是联通的,需要判断一下
code
//lg p3366
#include<bits/stdc++.h>
using namespace std;
const int N = 5005, M = (int)2e5+5;
struct edge{int u,v,w;}e[M];
int fa[N],sum,n,m;
int Find(int x){return fa[x]==x ? x : fa[x]=Find(fa[x]);}
void Union(int x,int y){if(Find(x)!=Find(y)) fa[Find(y)]=Find(x);}
bool cmp(edge x,edge y){return x.w<y.w;}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i=1;i<=m;i++)
{
cin>>e[i].u>>e[i].v>>e[i].w;
}
sort(e+1,e+1+m,cmp);
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1,cnt=0;i<=m;i++)
{
if(cnt>=n-1) break;
if(Find(e[i].u)!=Find(e[i].v))
{
cnt++;
Union(e[i].u,e[i].v);
sum+=e[i].w;
}
}
int check=0;
for(int i=1;i<=n;i++) if(fa[i]==i) check++;
if(check==1) cout<<sum;
else cout<<"orz";
return 0;
}
Prim
时间复杂度\(O(m\log{m})\)
算法流程
1.把点分成在树中和不在树中
2.选择不在树中距离树最近的点加入树中并更新
3.重复2直到达到跳出条件
注意
1.重载运算符小于号要反着写
code
//lg p3366
#include<bits/stdc++.h>
using namespace std;
const int N = 5005;
const int M = (int)2e5+5;
int n,m,dis[N],vis[N],sum,cnt;
int edgeid,head[N];
struct edge{int v,nxt,w;}e[M*2];
void addedge(int u,int v,int w){edgeid++;e[edgeid].v=v;e[edgeid].w=w;e[edgeid].nxt=head[u];head[u]=edgeid;}
struct node{int id,d;};
priority_queue<node> pq;
bool operator<(node x,node y) {return x.d>y.d;}
void Prim()
{
for(int i=1;i<=n;i++) dis[i]=0x3f3f3f3f;
dis[1]=0;
pq.push((node){1,0});
while(!pq.empty() && cnt<n)
{
node x=pq.top();
pq.pop();
int u=x.id;
if(!vis[u])
{
vis[u]=1;
cnt++;
sum+=dis[u];
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(dis[v]>e[i].w && !vis[v]) {dis[v]=e[i].w;pq.push((node){v,dis[v]});}
}
}
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i=1;i<=m;i++){int u,v,w;cin>>u>>v>>w;addedge(u,v,w);addedge(v,u,w);}
Prim();
if(cnt!=n){cout<<"orz";return 0;}
cout<<sum;
return 0;
}
最短路
Dijkstra
时间复杂度\(O(m\log{m})\)
算法流程
1.把点分为已求出最短路和未求出最短路
2.找到未求出最短路且距离已求出最短路的点最近的点加入最短路并更新
3.重复2直到堆空
code
//lg 3371
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = (int)1e5+5;
const int M = (int)2e5+5;
int n,m,st;
ll dis[N];
int edgeid,head[N];
bool vis[N];
struct edge{int v,w,nxt;}e[M*2];
void addedge(int u,int v,int w){edgeid++;e[edgeid].v=v;e[edgeid].w=w;e[edgeid].nxt=head[u];head[u]=edgeid;}
struct node {int id;ll d;};
bool operator<(node x,node y){return x.d>y.d;}
priority_queue<node> pq;
void Dij()
{
for(int i=1;i<=n;i++) dis[i]=0x3f3f3f3f3f3f3f3f;
dis[st]=0;
pq.push((node){st,0});
while(!pq.empty())
{
if(vis[pq.top().id]) {pq.pop();continue;}
int u=pq.top().id; pq.pop();
vis[u]=1;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(dis[v]>dis[u]+e[i].w)
{
dis[v]=dis[u]+e[i].w;
pq.push((node){v,dis[v]});
}
}
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>st;
for(int i=1;i<=m;i++)
{
int u,v,w;
cin>>u>>v>>w;
addedge(u,v,w);
}
Dij();
for(int i=1;i<=n;i++) cout<<dis[i]<<" ";
return 0;
}
Spfa
时间复杂度\(O(km)\) (死了)
算法流程
1.把可能引起松弛操作的边加入队列
2.进行松弛操作
3.重复12直到队空
code
//lg p3371
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = (int)1e4+4;
const int M = (int)5e5+5;
int edgeid,head[N];
struct edge{int v,nxt,w;}e[M*2];
void addedge(int u,int v,int w){edgeid++;e[edgeid].v=v;e[edgeid].w=w;e[edgeid].nxt=head[u];head[u]=edgeid;}
int n,m,st;
bool inq[N];
ll dis[N];
deque<int> q;
void Spfa()
{
for(int i=1;i<=n;i++) dis[i]=0x3f3f3f3f3f3f3f3f;
dis[st]=0;
q.push_front(st);
inq[st]=1;
while(!q.empty())
{
int u=q.front();
q.pop_front();
inq[u]=0;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(dis[v]>dis[u]+e[i].w)
{
dis[v]=dis[u]+e[i].w;
if(!inq[v])
{
if(!q.empty() && dis[v]<dis[q.front()]) q.push_front(v);
else q.push_back(v);
inq[v]=1;
}
}
}
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>st;
for(int i=1;i<=m;i++) {int u,v,w;cin>>u>>v>>w;addedge(u,v,w);}
Spfa();
for(int i=1;i<=n;i++)
{
if(dis[i]!=0x3f3f3f3f3f3f3f3f) cout<<dis[i]<<" ";
else cout<<INT_MAX<<" ";
}
return 0;
}
Floyd
时间复杂度\(O(n^3)\)
算法流程
1.dp
2.三重for 依次枚举i,j,k g[i][j]=min(g[i][j],g[i][k]+g[k][j])
code
//lg B3647
#include<bits/stdc++.h>
using namespace std;
const int N = 105;
int g[N][N],n,m;
void Floyd()
{
for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) g[i][j]=min(g[i][j],g[i][k]+g[k][j]);
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) g[i][j]=(int)1e9;
for(int i=1;i<=n;i++) g[i][i]=0;
for(int i=1;i<=m;i++) {int u,v,w;cin>>u>>v>>w;g[u][v]=min(g[u][v],w);g[v][u]=min(g[v][u],w);}
Floyd();
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++) cout<<g[i][j]<<" ";
cout<<endl;
}
return 0;
}
Johnson
时间复杂度\(O(nm\log{m})\)
算法流程(极简版)
1.超级源点向外连边,SPFA求势能
2.改边
3.每个点跑Dijkstra
code
//lg p5905
#include<bits/stdc++.h>
using namespace std;
const int INF = (int)1e9;
const int N =(int)3e3+3;
const int M =(int)6e3+4;
struct bian{int u,v,w;} b[M*2];
struct node{int id,v;};
bool operator<(node x,node y){return x.v>y.v;}
int eid,head[N],h[N],vis[N],inq[N],d[N],n,m;
priority_queue<node> pq;
queue<int> q;
struct edge{int v,nxt,w;}e[M*2];
void addedge(int u,int v,int w)
{
eid++;
e[eid].v=v;
e[eid].w=w;
e[eid].nxt=head[u];
head[u]=eid;
}
bool Spfa()
{
for(int i=1;i<=n;i++) h[i]=INF;
q.push(0);
inq[0]=1;
while(!q.empty())
{
int u=q.front();
q.pop();
inq[u]=0;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(h[v]>h[u]+e[i].w)
{
h[v]=h[u]+e[i].w;
if(!inq[v])
{
inq[v]=1;
q.push(v);
vis[v]++;
if(vis[v]>=n+1) return 0;
}
}
}
}
return 1;
}
void Dijkstra(int st)
{
for(int i=1;i<=n;i++) d[i]=INF,vis[i]=0;
d[st]=0;
pq.push((node){st,0});
while(!pq.empty())
{
int u=pq.top().id;
pq.pop();
if(vis[u]) continue;
vis[u]=1;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(d[v]>d[u]+e[i].w)
{
d[v]=d[u]+e[i].w;
if(!vis[v]) pq.push((node){v,d[v]});
}
}
}
return ;
}
signed main()
{
// freopen("working.in","r",stdin);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v,w;
cin>>u>>v>>w;
addedge(u,v,w);
}
for(int i=1;i<=n;i++) addedge(0,i,0);
if(!Spfa())
{
cout<<-1;
return 0;
}
for(int k=1;k<=n;k++)
{
for(int i=head[k];i;i=e[i].nxt) e[i].w=e[i].w+h[k]-h[e[i].v];
}
for(int i=1;i<=n;i++)
{
long long sum=0;
Dijkstra(i);
for(int j=1;j<=n;j++)
{
if(d[j]==INF) sum+=(long long )INF*j;
else sum+=(long long)j*(d[j]-h[i]+h[j]);
}
cout<<sum<<endl;
}
return 0;
}
欧拉路
时间复杂度 \(O(m\log{m})\)
前置知识
欧拉路径:图中所有边恰好经过一次的路径叫欧拉路径(一笔画)
欧拉回路:起点与终点相同的欧拉路径叫欧拉回路(可以一直画的一笔画)
有向图欧拉路径:有且仅有一个终点入度比出度多1 有且仅有一个起点出度比入度多1
有向图欧拉回路:所有点出入度相等
无向图欧拉路径:有且仅有两个点度数为奇数
无向图欧拉回路:所有点度数相等
算法流程
1.判断是否有解 记录起点终点
2.记忆当前边 跑Dfs
注意
1.建边细节
2.cmp细节
3.当前弧优化(雾
code
//lg p7771
#include<bits/stdc++.h>
using namespace std;
const int N = (int)1e5+5;
const int M = (int)2e5+5;
int edgeid,head[N];
struct edge{int v,nxt;}e[M];
void addedge(int u,int v){edgeid++;e[edgeid].v=v;e[edgeid].nxt=head[u];head[u]=edgeid;}
bool vis[M];
stack<int> stk;
struct Bian{int u,v;}bian[M];
int rin[N],rout[N],n,m,st,ed,cur[N];
void Dfs(int u)
{
for(int i=cur[u];i;i=cur[u])
{
cur[u]=e[i].nxt;
Dfs(e[i].v);
}
stk.push(u);
}
bool cmp(Bian x,Bian y) {return (x.u!=y.u) ? x.u<y.u : x.v>y.v;}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
bian[i].u=u;
bian[i].v=v;
rin[v]++;
rout[u]++;
}
for(int i=1;i<=n;i++)
{
if(rin[i]==rout[i]+1)
{
if(ed) return !(cout<<"No");
else ed=i;
}
if(rout[i]==rin[i]+1)
{
if(st) return !(cout<<"No");
else st=i;
}
}
if(!st) st=1;
sort(bian+1,bian+1+m,cmp);
for(int i=1;i<=m;i++)
{
addedge(bian[i].u,bian[i].v);
}
for(int i=1;i<=n;i++) cur[i]=head[i];
Dfs(st);
while(!stk.empty()) cout<<stk.top()<<" ",stk.pop();
return 0;
}
二分图
前置芝士
一般地:
\(|最大匹配|=|最小点覆盖|\) (二分图)
\(|最小边覆盖|+|最大匹配|=|点集|\)(无孤立点的图)
\(最大独立集+最小点覆盖=点集\)(任意图)
在二分图中:
\(|最大匹配|=|最小点覆盖|=a\)
\(|最小边覆盖|=|最大独立集|=b\)
\(a+b=|点集|\)
二分图判定
1.染色判定
2.无奇环是一个图是二分图的充要条件
匈牙利算法
时间复杂度\(O(nm)\)
算法流程
1.以下是跑Ntr算法要用的一些东西
如果题里没直接给,应该想办法先搞出来
左边的点(和右边的点)
左边的点指向右边的点的边
vis数组 用于记录每次增广中一个右边的点是否已经被访问(如果已经访问过就没必要再访问了)
match数组 用于记录每一个右边点现在匹配的左边点
2.递归调用,过于显然
code
//lg p3386
#include<bits/stdc++.h>
using namespace std;
const int N= 505;
const int M= (int)5e4;
int n,m,c;
int g[505][505];
int match[N];
bool vis[N];
int ans;
bool Ntr(int u)
{
for(int i=1;i<=m;i++)
{
if(g[u][i]==1 && !vis[i])
{
vis[i]=1;
if(!match[i] || Ntr(match[i]))
{
match[i]=u;
return 1;
}
}
}
return 0;
}
int main()
{
cin>>n>>m>>c;
for(int i=1;i<=c;i++)
{
int u,v;
scanf("%d%d",&u,&v);
g[u][v]=1;
}
for(int i=1;i<=n;i++)
{
memset(vis,0,sizeof(vis));
if(Ntr(i)) ans++;
}
cout<<ans;
return 0;
}
网络流
Dinic
时间复杂度\(O(n^2m)\)
算法流程
1.递归调用,都很显然
code
//lg p3376
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 205;
const int M = 5005;
int n,m;
int st,ed;
int edgeid=1;
int head[N];
struct edge
{
int v,nxt;
ll w;
}e[M*2];
void addedge(int u,int v,ll w)
{
edgeid++;
e[edgeid].v=v;
e[edgeid].w=w;
e[edgeid].nxt=head[u];
head[u]=edgeid;
edgeid++;
e[edgeid].v=u;
e[edgeid].w=0;
e[edgeid].nxt=head[v];
head[v]=edgeid;
}
int dep[N];
int cur[N];
queue<int> q;
bool Bfs()
{
while(!q.empty()) q.pop();
q.push(st);
for(int i=1;i<=n;i++) cur[i]=head[i],dep[i]=0;
dep[st]=1;
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(e[i].w && !dep[v])
{
dep[v]=dep[u]+1;
if(v==ed) return 1;
q.push(v);
}
}
}
return 0;
}
ll Dfs(int u,ll rst)
{
if(u==ed || !rst) return rst;
ll sum=0;
for(int i=cur[u];i;i=e[i].nxt)
{
int v=e[i].v;
cur[u]=i;
ll f;
if(dep[v]==dep[u]+1 && (f=Dfs(v,min(rst,e[i].w))) )
{
sum+=f;
rst-=f;
e[i].w-=f;
e[i^1].w+=f;
if(!rst) break;
}
}
if(!sum) dep[u]=0;
return sum;
}
int main()
{
cin>>n>>m>>st>>ed;
for(int i=1;i<=m;i++)
{
int u,v;
ll w;
scanf("%d%d%lld",&u,&v,&w);
addedge(u,v,w);
}
ll ans=0;
while(Bfs())
{
ans+=Dfs(st,LLONG_MAX);
}
cout<<ans;
return 0;
}
网络流求二分图最大匹配
时间复杂度\(O(\sqrt{n}m)\)
建边部分
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>c;
st=0;
ed=n+m+1;
for(int i=1;i<=n;i++) addedge(st,i,1);
for(int i=n+1;i<=n+m;i++) addedge(i,ed,1);
for(int i=1;i<=c;i++)
{
int u,v;
cin>>u>>v;
addedge(u,v+n,1);
}
Dinic();
cout<<ans;
return 0;
}
DAG最小链覆盖
DAG最小不可重链覆盖
DAG最小不可重链覆盖的解法如下:把所有结点i拆为结点i和结点′i ,如果图G中存在有向边i⇒j,则在二分图中引入边i⇒′j 。设二分图的最大匹配数为m,则结果就是n−m。为什么呢?因为匹配和链覆盖是一一对应的。对于链覆盖中的每条链,除了最后一个“结尾结点”之外都有唯一的后继与它对应(即匹配结点),因此匹配数就是非结尾结点的个数。当匹配数达到最大时,非结尾结点的个数也将达到最大。此时,结尾结点的个数最少,即链条数最少
DAG最小可重链覆盖
我们一般先用Floyd求原图的传递闭包,接着求这个闭包偏序集的最小不可重链覆盖
正确性很显然
Dinic优化
时间复杂度
随机图:\(O(km)(k约为10)\)
上界:\(O(n^2m)(实际上构造出O(nm)的数据都是极难的)\)
优化流程
1.离线,对边按权从大到小排序
2.二进制位数相同的边称为一组,按位数从大到小,对每个不同的二进制位位数执行3
3.加入这组边,跑Dinic(不加反向边)
4.加入所有反向边,跑Dinic
//lg p3376
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1205;
const int M = 120005;
int n,m;
int st,ed;
ll ans;
bool rev;
int edgeid=1;
int head[N];
struct edge
{
int v,nxt;
ll w;
}e[M*2];
void addedge(int u,int v,ll w)
{
edgeid++;
e[edgeid].v=v;
e[edgeid].w=w;
e[edgeid].nxt=head[u];
head[u]=edgeid;
edgeid++;
e[edgeid].v=u;
e[edgeid].w=0;
e[edgeid].nxt=head[v];
head[v]=edgeid;
}
struct bian {int u,v;ll w;} b[M*2];
bool cmp(bian x,bian y) {return x.w>y.w;}
int dep[N];
int cur[N];
queue<int> q;
bool Bfs()
{
while(!q.empty()) q.pop();
q.push(st);
for(int i=1;i<=n;i++) cur[i]=head[i],dep[i]=0;
dep[st]=1;
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=head[u];i;i=e[i].nxt)
{
if(i&1&rev) continue;
int v=e[i].v;
if(e[i].w && !dep[v])
{
dep[v]=dep[u]+1;
if(v==ed) return 1;
q.push(v);
}
}
}
return 0;
}
ll Dfs(int u,ll rst)
{
if(u==ed || !rst) return rst;
ll sum=0;
for(int i=cur[u];i;i=e[i].nxt)
{
if(i&1&rev) continue;
int v=e[i].v;
cur[u]=i;
ll f;
if(dep[v]==dep[u]+1 && (f=Dfs(v,min(rst,e[i].w))) )
{
sum+=f;
rst-=f;
e[i].w-=f;
e[i^1].w+=f;
if(!rst) break;
}
}
if(!sum) dep[u]=0;
return sum;
}
void Dinic(){while(Bfs()) ans+=Dfs(st,LLONG_MAX);}
int main()
{
// freopen("working.in","r",stdin);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>st>>ed;
for(int i=1;i<=m;i++) cin>>b[i].u>>b[i].v>>b[i].w;
sort(b+1,b+m+1,cmp);
rev=1;
for(int k=32,i=0;k>=1 && i<=m;k--)
{
while(b[i+1].w>=(1<<(k-1)) && i<m)
{
i++;
addedge(b[i].u,b[i].v,b[i].w);
}
Dinic();
}
rev=0;
Dinic();
cout<<ans;
return 0;
}
预流推进(HLPP)
废话
1.某人发现洛谷预流推进板子题几乎没有强调了所有常见错误且完全正确的的HLPP通用题解,于是水了一篇上去,所以里面讲的相比其他地方很详细(
2.常见的最大流算法可以大致分为两种:找增广路 和 预流推进
3.从理论时间复杂度上界来看,找增广路算法的最坏时间复杂度为\(O(n^2m)\),而预流推进算法的最坏时间复杂度为\(O(n^2\sqrt{m})\),看起来预流推进要显然优于找增广路,但在实际应用(尤指OI)中,由于包括但不限于以下两个原因,预流推进的使用频率远不如Dinic等找增广路算法。
网络最大流这一类型题的难点一般在于建模而不是算法本身,故只要出题人不毒瘤,一般不会给出只有预流推进算法才能通过的数据范围
对于随机图,Dinic等找增广路算法的实际表现可能不输于预流推进(类似于SPFA与Dijkstra的关系)
大体思想
接下来我们介绍预流推进算法中常用的一种:HLPP
想必看这篇题解的人都已经学会了找增广路式网络流算法,在找增广路算法与预留推进算法较为相似的部分,我们可能略讲,区别之处,我们则详细讲解。我们现在继续借用“水流”“水管”等物象辅助理解HLPP算法
超额流
不同于找增广路算法全程保证了流的合法性,在预流推进算法中,顾名思义,可以“预测”将来的流量,也不用保证流的合法性。
具体地,我们除了汇源点之外的点都看作水库,其中可以存储无限多“预测将要流出”的流量,不难想到,当算法执行到最后得到合法的流方案时,所有“水库”储存的“流”都应该为0(因为流进来多少就流出去多少)
高度
与找增广路算法给图分层类似的,预流推进算法中引入了高度这一概念,就像找增广路算法中流只能从前一层流向后一层,预流推进算法中的流只能从高度为\(high\)的点流向高度为\(high-1\)的点
这时考虑一种尴尬的情况,如果有个点的的高度为\(high\),但是它所连接的所有点中没有既满足边流量未满又满足高度为\(high-1\)的,也就是“水库”里的水流不出去了。
那么,如何科学的设定/维护/改变所有节点的高度呢?
重贴标签
解决的思路非常简单,只要找到一个高度的值,将点的高度改为这个值,让“水库”里的水正好可以往外流就可以啦
代码实现
1.我们维护一个优先队列,每次只取出高度最高的点进行操作(因为只有最高点的高度可能变化,所以不必担心优先队列里已经排序的数据发生错误)
2.每次从队头取出高度最高的点u,对u连接的每个v尝试推流,如果v不在队中则v入队,再对u的”水库“进行检查,因为我们最后要使每个点的“水库”为0,所以我们需要对u进行重贴标签并再次入队,以使“水库”中的水流出去(注意,推流到汇点的流计入答案,推流到源点的流则作废)
3.重复执行2直到队空
优化常数
BFS
在正式开始HLPP之前,我们可以先对原图进行Bfs为所有点划定一个大概的初始高度,这样可以避免反复的高度变化,节省了大量时间
GAP
如果发现没有任何一个点的高度等于某一个值,则说明比这个高度高的所有点均无法推流,这时可以把所有高度比这个值大且比n+1小(源点高度为n)的点的高度都直接设为n+1,以便这些点快速把流退回源点结束算法。
具体地,如果我们在给某个点重贴标签的过程中发现这个点所在的高度没有其他点了,这时我们使用gap优化直接改变一些点的高度,由于“某个点”的高度是队中最高,所以这个优化对优先队列中的数据没有影响。
code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int INF = 0x3f3f3f3f;
const ll LLINF = 0x3f3f3f3f3f3f3f3f;
const int N= 1205;
const int M= 120005;
ll x[N];
int n,m,st,ed,h[N],head[N],gap[N*2],edgeid=1;
bool inq[N];
struct cmp
{
bool operator()(int x,int y) const {return h[x]<h[y];}
};
priority_queue<int,vector<int>,cmp> pq;
queue<int> q;
struct edge{int v,w,nxt;}e[M*2];
inline int Read()
{
int ans=0;
bool flag=0;
char ch=getchar();
while(!isdigit(ch) && ~ch)
{
flag |= (ch=='-');
ch=getchar();
}
while(isdigit(ch) && ~ch)
{
ans=(ans<<1)+(ans<<3)+(ch^48);
ch=getchar();
}
if(flag) ans=-ans;
return ans;
}
inline void addedge(int u,int v,int w)
{
edgeid++;
e[edgeid].v=v;
e[edgeid].w=w;
e[edgeid].nxt=head[u];
head[u]=edgeid;
edgeid++;
e[edgeid].v=u;
e[edgeid].w=0;
e[edgeid].nxt=head[v];
head[v]=edgeid;
}
bool Bfs()
{
for(int i=1;i<=n;i++) h[i]=INF;
h[ed]=0;
q.push(ed);
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=head[u];i;i=e[i].nxt) if(e[i^1].w && h[e[i].v]>h[u]+1) h[e[i].v]=h[u]+1,q.push(e[i].v);
}
return (h[st]!=INF);
}
void Push(int u)
{
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(e[i].w && h[v]+1==h[u])
{
int f;
f=min((ll)e[i].w,x[u]);
x[u]-=f,x[v]+=f,e[i].w-=f,e[i^1].w+=f;
if(v!=st && v!=ed && !inq[v]) pq.push(v),inq[v]=1;
if(!x[u]) break;
}
}
}
void Relabel(int u)
{
h[u]=INF;
for(int i=head[u];i;i=e[i].nxt) if(e[i].w && h[e[i].v]+1<h[u]) h[u]=h[e[i].v]+1;
}
void Hlpp()
{
if(!Bfs()) return ;
h[st]=n;
for(int i=0;i<=2*n;i++) gap[i]=0;
for(int i=1;i<=n;i++) if(h[i]<INF) gap[h[i]]++;
for(int i=head[st];i;i=e[i].nxt)
{
int f,v=e[i].v;
if((f=e[i].w) && h[v]<INF)
{
e[i].w-=f;
e[i^1].w+=f;
x[st]-=f;
x[v]+=f;
if(v!=st && v!=ed && !inq[v]) pq.push(v),inq[v]=1;
}
}
while(!pq.empty())
{
int u=pq.top();
inq[u]=0;
pq.pop();
Push(u);
if(x[u])
{
gap[h[u]]--;
if(!gap[h[u]]) for(int i=1;i<=n;i++) if(i!=st && i!=ed && h[i]>h[u] && h[i]<n+1) h[i]=n+1;
Relabel(u);
gap[h[u]]++;
pq.push(u);
inq[u]=1;
}
}
}
signed main()
{
// freopen("working.in","r",stdin);
n=Read();m=Read();st=Read();ed=Read();
for(int i=1;i<=m;i++)
{
int u,v,w;
u=Read();v=Read();w=Read();
addedge(u,v,w);
}
Hlpp();
cout<<x[ed];
return 0;
}
重要细节
GAP
点的最大高度可能达到\(2n-1\),gap优化数组的大小要开\(2n\)
无脑入队
由于我们对源点的推流是特殊处理的,即对于跟源点相连的点会有无脑入队的情况。如果恰好这时候被入队的点与汇点之间没有路径,这个点的高度没有被更新过(INF),再进行gap优化就会RE 所以我们在处理源点入队要加一个限制条件,即h[i]<INF
爆int
即使这套题的数据范围看起来处处表明这题不用开long long,但由于预流推进执行过程中流可能不合法,即可能会有点的“水库”存了超过int范围的流,所以要对存每个点“水库”大小的数组开long long
最小割
显然,最小割=最大割
建议找一张图来感受一下
“不要试图理解它,而是感受它”
没什么卵用还看不懂一点的证明(OI wiki)
二选一模型
例题引入:
有\(N\)个任务,每个任务可以在机器 A 或机器 B 上完成,花费分别为\(a_i\)或\(b_i\) 。有\(M\)对关系,每对关系为二元组\((x,y)\),表示第\(i\)个任务和第\(j\)个任务如果在不同的机器上完成,则增加\(v\)的花费。问如何安排完成每个任务的机器,使总花费最小。
构造
1.转化为最小割,跟起点在一起记为机器A,跟终点在一起记为机器B
2.st向每个点连边,如果割掉即为机器B,所以这条边权值为\(b_i\)
同理,每个点向ed连边,如果割掉即为机器A,所以这条边边权为\(a_i\)
3.对于每个二元组\((x,y)\),在\(x\),\(y\)之间连双向边,如果割掉任意一条即为不在同一个机器,所以这条边边权为\(v\)
4.跑最大流,即为最小花费
最大权闭合子图
前置芝士
闭合子图:闭合子图是这样一个点集\(V'\),满足\(V'\)是\(V\)的子集且对于任意的\(u∈V'\)的任意出边\((u,v)∈E\)必然有\(v∈V'\)成立
最大权闭合子图:给每个点赋一个点权,最大权闭合子图就是一个点权之和最大的闭合子图
构造
1.首先假设所有正权点都选,所有负权点都不选,答案初始值为正点权之和
2.转化为求最小割,跟起点在同一个集合里的点为记为选,跟终点在一个集合里的点记为不选
3.若w[i]>0 连边st->i权值为w[i] 意义为如果不选i(割掉这条边)代价为w[i]
若w[i]<0 连边i->ed权值为-w[i] 意义为如果选了i(割掉这条边)代价为-w[i]
4.求最小割(最大流)的结果即为最小代价,答案减去这个代价即为最大闭合权子图的最大权,最后一次Bfs能访问到的点即为跟起点在一起的点集
最小割集
性质
关于最小割的可行边,以下命题等价
1.一条边\(x,y\)可以在最小割中
2.\(x,y\)的容量减少后最大流(最小割)也减小了
3.跑完最大流的残量网络中,不存在\(x->y\)的路径
4.\(x,y\)满流,且\(x,y\)在跑完最大流的残量网络上不属于同一个SCC
关于最小割的必须边,以下命题等价
1.一条边\(x,y\)必须在最小割中
2.\(x,y\)的容量增大后最大流(最小割)也增大了
3.跑完最大流的残量网络中,存在\(S->x,y->T\)的路径
4.跑完最大流的残量网络中,\(S,x\)同属一个SCC,\(y,T\)同属一个SCC
算法分析
假设源点为\(S\),汇点为\(T\),最小割将点集分为两个点集\(S\)、\(T\)。
首先,点集\(S\)、\(T\)不一定是唯一的。
点集\(S\)的一种情况:在残留网络中从源点出发,能走到的所有点都属于\(S'\)。
点集\(T\)的一种情况:在残留网络的反图(原图的边\((u,v)\)在反图中就变成\((v,u)\))中从汇点出发,能走到的所有点都属于\(T'\)。
显然,如果一个点既不属于\(S'\)也不属于\(T'\),这个点可以属于\(S\)也可以属于\(T\),最小割是不唯一的
又很显然,如果没有任意一个点既不属于\(S'\)也不属于\(T'\),那么最小割唯一
费用流
原始对偶
!!!警钟长鸣!!!
原始对偶套Dinic是这个世界上最傻缺的事,没有之一
不是不能套,而是越套越慢,如果再套一个消圈算法就会出现理论复杂度上界更低,但实际常数炸到稳定TLE的诡异情况
(别问我怎么知道的,说多了都是泪)
算法流程
1.Spfa求初始势
2.Dijkstra+EK增广
时间复杂度\(O(快的飞起)\)
注意
1.细节很多
code
//lg p3381
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = (int)5e3+4, M =(int)5e4+4;
ll mxflow,mncost;
int h[N],d[N],vis[N],inq[N],id=1,n,m,head[N],pre[N],S,T;
struct node{int u,d;};
bool operator<(node x,node y) {return x.d>y.d;}
struct edge{int v,w,c,nxt;}e[M*2];
void Adde(int u,int v,int w,int c){id++;e[id].v=v;e[id].w=w;e[id].c=c;e[id].nxt=head[u];head[u]=id;id++;e[id].v=u;e[id].w=0;e[id].c=-c;e[id].nxt=head[v];head[v]=id;}
priority_queue<node>pq;
queue<int> q;
void Spfa()
{
for(int i=1;i<=n;i++) h[i]=0x3f3f3f3f;
inq[S]=1;
q.push(S);
h[S]=0;
while(!q.empty())
{
int u=q.front();
q.pop();
inq[u]=0;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(h[v]>h[u]+e[i].c && e[i].w)
{
h[v]=h[u]+e[i].c;
if(!inq[v])
{
q.push(v);
inq[v]=1;
if(h[q.front()]>h[q.back()]) swap(q.front(),q.back());
}
}
}
}
}
bool Dijkstra()
{
for(int i=1;i<=n;i++) d[i]=0x3f3f3f3f,vis[i]=0;
d[S]=0;
pq.push((node){S,d[S]});
while(!pq.empty())
{
int u=pq.top().u;
pq.pop();
if(vis[u]) continue;
vis[u]=1;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v,c=e[i].c+h[u]-h[v];
if(d[v]>d[u]+c && e[i].w)
{
d[v]=d[u]+c,pre[v]=i;
pq.push((node){v,d[v]});
}
}
}
return d[T]<0x3f3f3f3f;
}
void Mcmf()
{
Spfa();
while(Dijkstra())
{
for(int i=1;i<=n;i++) h[i]+=d[i];
int f=0x3f3f3f3f;
for(int u=T;u!=S;u=e[pre[u]^1].v) f=min(f,e[pre[u]].w);
for(int u=T;u!=S;u=e[pre[u]^1].v) e[pre[u]].w-=f,e[pre[u]^1].w+=f;
mxflow+=f;
mncost+=(ll)f*h[T];
}
}
int main()
{
// freopen("working.in","r",stdin);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>S>>T;
for(int i=1;i<=m;i++)
{
int u,v,w,c;
cin>>u>>v>>w>>c;
Adde(u,v,w,c);
}
Mcmf();
cout<<mxflow<<" "<<mncost;
return 0;
}
消负费用圈算法
算法流程
1.所有负费用边强制满流,提前加上费用,借用预流推进思想,每个点建立水库
2.建立辅助源汇点SS,TT
3.水库存水为正的,SS向水库连费用为0容量为水库水量的边
水库存水为负的,水库向TT连费用为0容量为水库水量的相反数的边
4.对辅助源汇点跑Mcmf,清空mxflow(因为返回操作的存在,最大流实际不受影响,但是最小费用会)
5.对源汇点跑Mcmf
注意
1.算法执行过程中总点数因为辅助源汇点的加入发生变化,请妥善处理n的值与程序初始化部分
存疑
1.yeyou26发现网上部分讲解中再算法流程第3步后,从汇点向源点连接一条费用0容量无限的边,经测试,注释掉这一行可以通过洛谷板子题且效率无差异,yeyou26也想不明白连这条边的道理何在
时间复杂度O(反正能过)
code
//lg p7173
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = (int)5e3+4, M =(int)5e4+4;
ll mxflow,mncost;
int h[N],d[N],vis[N],inq[N],id=1,n,m,head[N],pre[N],S,T,x[N],SS,TT;
struct node{int u,d;};
bool operator<(node x,node y) {return x.d>y.d;}
struct edge{int v,w,c,nxt;}e[M*2];
void Adde(int u,int v,int w,int c){id++;e[id].v=v;e[id].w=w;e[id].c=c;e[id].nxt=head[u];head[u]=id;id++;e[id].v=u;e[id].w=0;e[id].c=-c;e[id].nxt=head[v];head[v]=id;}
void Adde_(int u,int v,int w,int c){id++;e[id].v=v;e[id].w=w;e[id].c=c;e[id].nxt=head[u];head[u]=id;}
priority_queue<node>pq;
queue<int> q;
void Spfa(int s,int t)
{
for(int i=1;i<=n;i++) h[i]=0x3f3f3f3f;
inq[s]=1;
q.push(s);
h[s]=0;
while(!q.empty())
{
int u=q.front();
q.pop();
inq[u]=0;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(h[v]>h[u]+e[i].c && e[i].w)
{
h[v]=h[u]+e[i].c;
if(!inq[v])
{
q.push(v);
inq[v]=1;
if(h[q.front()]>h[q.back()]) swap(q.front(),q.back());
}
}
}
}
}
bool Dijkstra(int s,int t)
{
for(int i=1;i<=n;i++) d[i]=0x3f3f3f3f,vis[i]=0;
d[s]=0;
pq.push((node){s,d[s]});
while(!pq.empty())
{
int u=pq.top().u;
pq.pop();
if(vis[u]) continue;
vis[u]=1;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v,c=e[i].c+h[u]-h[v];
if(d[v]>d[u]+c && e[i].w)
{
d[v]=d[u]+c,pre[v]=i;
pq.push((node){v,d[v]});
}
}
}
return d[t]<0x3f3f3f3f;
}
void Mcmf()
{
Spfa(SS,TT);
while(Dijkstra(SS,TT))
{
for(int i=1;i<=n;i++) h[i]+=d[i];
int f=0x3f3f3f3f;
for(int u=TT;u!=SS;u=e[pre[u]^1].v) f=min(f,e[pre[u]].w);
for(int u=TT;u!=SS;u=e[pre[u]^1].v) e[pre[u]].w-=f,e[pre[u]^1].w+=f;
mxflow+=f;
mncost+=(ll)f*h[TT];
}
Spfa(S,T);
mxflow=0;
while(Dijkstra(S,T))
{
for(int i=1;i<=n;i++) h[i]+=d[i];
int f=0x3f3f3f3f;
for(int u=T;u!=S;u=e[pre[u]^1].v) f=min(f,e[pre[u]].w);
for(int u=T;u!=S;u=e[pre[u]^1].v) e[pre[u]].w-=f,e[pre[u]^1].w+=f;
mxflow+=f;
mncost+=(ll)f*h[T];
}
}
int main()
{
// freopen("working.in","r",stdin);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>S>>T;
for(int i=1;i<=m;i++)
{
int u,v,w,c;
cin>>u>>v>>w>>c;
if(c>0) Adde(u,v,w,c);
else
{
Adde_(u,v,0,c);
Adde_(v,u,w,-c);
x[v]+=w,x[u]-=w;
mncost+=w*c;
}
}
SS=++n,TT=++n;
for(int i=1;i<=n-2;i++)
{
if(!x[i]) continue;
else if(x[i]>0) Adde(SS,i,x[i],0);
else Adde(i,TT,-x[i],0);
}
//Adde(T,S,0x3f3f3f3f,0);
Mcmf();
cout<<mxflow<<" "<<mncost;
return 0;
}
联通问题
SCC
懒得写了
时间复杂度\(O(n)\)
code
//强连通分量
//lg 2863 求强连通分量的数量
#include<bits/stdc++.h>
using namespace std;
const int N = (int)2e4+4;
int where[N];//这个点在哪个scc里
int scccnt;
int sccsize[N];
int low[N],dfn[N],idx;
bool instk[N];
stack<int> stk;
vector<int> e[N];
int n,m;
void Tarjan(int u)
{
dfn[u]=low[u]=++idx;
instk[u]=true;
stk.push(u);
for(int v : e[u])
{
if(!dfn[v])
{
Tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(instk[v])
{
low[u]=min(low[u],dfn[v]);
//low[u]=min(low[u],low[v]);
}
}
if(dfn[u]==low[u])
{
scccnt++;
while(stk.top()!=u)
{
instk[stk.top()]=false;
where[stk.top()]=scccnt;
sccsize[scccnt]++;
stk.pop();
}
sccsize[scccnt]++;
where[u]=scccnt;
instk[u]=false;
stk.pop();
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
e[u].push_back(v);
}
for(int i=1;i<=n;i++) Tarjan(i);
int cnt=0;
for(int i=1;i<=scccnt;i++) if(sccsize[i]>1) cnt++;
cout<<cnt;
return 0;
}
最大半联通子图
算法流程
首先Tarjan缩点,很显然问题变成了求新图的最长点权链
记录每个点的最长链长度,达成此长度的方案数什么的,按照拓扑序简单DP即可
值得注意的是,终点那块可能是分叉的,所以我们需要枚举终点
2-SAT
基本常识
\(i->j\) 这样的连边代表如果选 \(i\) 一定选 \(j\)
显然我们有以下结论:
1.若有\(i->j\) 则有 \(j'->i'\)
2.若有\(i->j \quad j->k\) 则有 \(i->k\)
3.若 \(i,i'\) 在同一个SCC中,则无解
4.Tarjan求得的SCC的编号为拓扑逆序
约束条件及常见连边
设变量 \(xi,xj\),\(i,j\) 表示变量取正值,\(i',j'\) 表示变量取反值
1.\(xi\) 必为真 连边 \(i'->i\)
2.\(xi\) 必为假 连边 \(i->i'\)
3.\(xi与xj\) 至少有一个为真 连边 \(i'->j \quad j'->i\)
4.\(xi与xj\) 至少有一个为假 连边 \(i->j' \quad j->i'\)
5.\(xi与xj\) 相同 连边 \(i->j \quad j->i \quad i'->j' \quad j'->i'\)
6.\(xi与xj\) 不同 连边 \(i->j' \quad j->i' \quad i'->j \quad j'->i\)
判定及构造解
解的判定:如果不存在 \(i,i'\) 在同一个SCC中,则一定有解,否则无解
构造解:
1.在求SCC时,每求出一个强连通分量判断该强连通分量是否可选(显然求出SCC的顺序就是拓扑逆序),如果该强连通分量中的每个元素都可选,纳昂么选择这个强连通分量并且把该强连通分量中所有元素的矛盾点设为不可选。
2.(存疑)对于每个布尔变量,检查它正反变量所在的SCC,把SCC编号小的点置为真