图论代码模板

这里记录了图论方面平时个人常用的代码模板。

图论

常见概念

  • 简单图:不含有平行边和自环的图。
  • 多重图:含有平行边(同向)的图。
  • 完全图:每一对节点之间都有边的简单图,$n$个节点无向完全图记为$K_n$。
  • 平面图:能将所有点和边画在平面上,且边与边不相交的无向图。
  • 补图:由$G$中所有节点和所有能使$G$成为完全图的添加边组成的图。
  • 生成子图(Spanning Subgraph):含有原图$G$中所有结点的子图。
  • 图同构的必要条件
    1. 节点数相同。
    2. 边数相同。
    3. 度数相同的结点数相同。

网络流常用概念

  • 连通图:只有一个连通分支(极大连通子图)的图。
  • 割点:无向连通图中一个顶点$v$,,若删除它以及关联的边后,得到的新图至少包含两个连通分支。
  • 桥:无向连通图中的一条边$e$,删除它得到的新图包含两个连通分量。
  • 团(Clique):原图$G$的一个完全子图。
  • 极大团(Maximal Clique):不是其它团的子图的团。
  • 最大团(Maximum Clique):顶点最多的极大团。
  • 诱导子图(Induced Subgraph):所有节点在原图$G$上连接的边都被包含在内的子图。
  • 独立集:在图上选出一些节点,这些节点间两两无边的点集。
  • 路径覆盖:在图中找一些路径,这些路径覆盖图中所有的顶点,每个顶点都只与一条路径相关联。
  • 最小染色数:用最少的颜色个数给点染色且任意两点相邻点颜色不同,最少的颜色个数。

弦图常用概念

  • 弦:连接环上两个不相邻节点的边。
  • 弦图:图上任意一个点数大于三的环都至少存在一条弦的无向图。
  • 单纯点:节点$v$与相邻节点的诱导子图是一个团。
  • 完美消除序列:有$n$个点的排列$v_1,v_2,\dots,v_n$,该排列满足$v_i$在${v_i,v_{i+1},\dots,v_n}$的诱导子图中是一个单纯点。
  • 一个无向图是弦图当且仅当它有一个完美消除序列

常见结论

  • 每个图中节点度数和等于边数的二倍,$\sum\limits_{v\in V} deg(v)=2\left| E \right|$。
  • 任何图中度数为奇数的节点必定有偶数个。
  • 完全图$K_n$的边数$=\frac{n(n-1)}{2}$
  • 最大团中顶点数量 = 补图的最大独立集中顶点数量
  • 最大团数 ≤ 最小染色数,最大独立集 ≤ 最小团覆盖
  • 弦图中:最大团数 = 最小染色数,最大独立集 = 最小团覆盖
  • 平面图边数$m\le 3n-6$。

弦图

最大势算法求消除序列并判定弦图

判断一个消除序列是否为完美消除序列:从后向前枚举序列中的点$v_i$,设序列中在$v_i$后且与$v_i$相邻的点集依次为${v_{j1},v_{j2},\dots,v_{jk}}$,判断$v_j1$是否与${v_{j2},\dots,v_{jk}}$相邻即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#include<bits/stdc++.h>
using namespace std;
const int maxn=1005;
struct edge
{
int v,nex;
edge(int v=0,int n=-1):
v(v),nex(n){}
} e[maxn*maxn];
int cnt=0,head[maxn];
inline void add(int u,int v)
{
e[++cnt].v=v;
e[cnt].nex=head[u];
head[u]=cnt;
}
int label[maxn],id[maxn],order[maxn];//id[i]表示节点i在序列中的编号
bool vis[maxn];//order[i]为完美消除序列第i个节点,label[x]表示x点与多少个已标号节点相邻
vector<int> vec[maxn];//vec[x]存储*与x个已标号节点相邻*的节点
void mcs(int n)//节点数量
{//复杂度O(n+m)
memset(vis,0,sizeof(vis));
memset(label,0,sizeof(label));
for(int i=1;i<=n;i++)
vec[0].push_back(i);
int maxx=0,now=0;
for(int i=1;i<=n;i++)
{//从前往后,每轮必定会找出一个
bool flag=0;
while(!flag)
{
for(int j=vec[maxx].size()-1;j>=0;j--)
{//从后往前找
if(vis[vec[maxx][j]])//该节点已经标记过
vec[maxx].pop_back();
else{
flag=1;//找到个未访问的节点
now=vec[maxx][j];
break;
}
}
if(!flag)
maxx--;
}
vis[now]=1;//
order[n-i+1]=now;
id[now]=n-i+1;//节点x在序列中的位置
for(int j=head[now];~j;j=e[j].nex)
{//遍历与now节点相邻的节点
int v=e[j].v;
if(!vis[v])
{
label[v]++;//v节点与已标号节点数++
vec[label[v]].push_back(v);
maxx=max(maxx,label[v]);//找出最大的那个节点
}
}
}
}
int bucket[maxn];
bool isperfect(int n)
{//复杂度O(n+m)
mcs(n);//计算消除序列并判断是否为完美消除序列
memset(vis,0,sizeof(vis));//判断序列中的点v_i是否与其后所有点相接
for(int i=n;i>0;i--)//序列从后向前
{
int tot=0,ret=1;//每轮桶清空
for(int j=head[order[i]];~j;j=e[j].nex)
if(id[e[j].v]>i)//排在序列编号x后面与x相邻的点集记为:N(x)
vis[bucket[++tot]=e[j].v]=1;//序列中x后且与x邻接点标记并放入桶中
for(int j=head[bucket[1]];~j;j=e[j].nex)//buc[1]的id为N(x)中最小?
{//bucket[1]=0...
int v=e[j].v;
if(v!=bucket[1]&&vis[v])
{//判断N(x)中排在最前面的点是否与N(x)其他点相邻
ret++;
}
}
for(int j=1;j<=tot;j++)
vis[bucket[j]]=0;//防止每次memset超时
if(tot&&ret!=tot)//不全部邻接
return 0;
}
return 1;
}
int main()
{
int n,m,u,v;
while(cin>>n>>m&&n&&m)
{
memset(head,-1,sizeof(head));
memset(order,0,sizeof(order));
cnt=0;
for(int i=1;i<=n;i++)
vec[i].clear();
for(int i=1;i<=m;i++)
{
cin>>u>>v;
add(u,v);
add(v,u);
}
cout<<(isperfect(n)?"Perfect\n\n":"Imperfect\n\n");
}
return 0;
}

三元环

步骤

  1. 对原无向图进行定向,对任何一条边,从度数大的点连向度数小的点,如果度数相同,从编号小的点连向编号大的点。得到一个有向无环图。
  2. 枚举每一个节点$u$,将$u$所有相邻节点打上$u$的标记。再枚举$u$的相邻节点$v$,枚举$v$的相邻节点的相邻节点$w$,如果$w$上有被标记为$u$,则$(u,v,w)$就是一个三元环。

统计图上三元环数量,复杂度$O(m\sqrt{m})$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
vector G[maxn];//新图
int vis[maxn],deg[maxn];
int cycle3(int n)
{
int ans=0;
for(auto &e:edges)
{//统计原无向图度数
deg[e.u]++;
deg[e.v]++;
}
for(auto &e:edges)//建立新图
if(deg[e.u]<deg[e.v]||(deg[e.u]==deg[e.v]&&e.u<e.v))
G[e.u].push_back(edge(e.u,e.v));
else
G[e.v].push_back(edge(e.v,e.u));
for(int u=1;u<=n;u++)
{
for(int v:G[u])
vis[v]=u;//相邻节点打上u的标记
for(int v:G[u])
for(int w:G[v])
if(vis[w]==u)
ans++;
}
return ans;
}

最短路

Dijkstra堆优化

复杂度$O(ElgE)$,稠密图中有时不如普通版优秀,但比SPFA快。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const int MAXN=1050;
struct qnode{
int v,c;
qnode(int _v=0,int _c=0):v(_v),c(_c){}
bool operator <(const qnode &r)const{
return c>r.c;//重载运算符<
}
};
struct Edge{
int v,cost;
Edge(int _v=0,int _cost=0):v(_v),cost(_cost){}
};
vector<Edge>E[MAXN];//使用前必须清空0~n
bool vis[MAXN];
int dist[MAXN];//到这个点的最近队员的
void Dijkstra(int n,int start)//传入总数及起点
{//点的编号从 1 开始
memset(vis,false,sizeof(vis));
for(int i=1;i<=n;i++)
dist[i]=inf;
priority_queue<qnode>que;
while(!que.empty())
que.pop();
dist[start]=0;
que.push(qnode(start,0));
while(!que.empty())
{
qnode now=que.top();
que.pop();
int u=now.v;
if(vis[u])
continue;
vis[u]=true;
for(int i=0;i<E[u].size();i++)
{
int v=E[u][i].v;
int cost=E[u][i].cost;
if(!vis[v]&&dist[v]>dist[u]+cost){
dist[v]=dist[u]+cost;
que.push(qnode(v,dist[v]));
}
}
}
}
void addedge(int u,int v,int w)
{
E[u].push_back(Edge(v,w));
}

SPFA

最坏复杂度$O(VE)$,V为节点数,不适用于稠密图

检测负环:当存在一个点入队大于等于$V$次,则有负环

BFS实现(求最短路)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const int inf=0x3f3f3f3f,MAXN=5051;
struct node
{
int v,w,next;
} e[MAXN];
int cnt,dist[MAXN],head[MAXN],num[MAXN];
bool vis[MAXN];
void add(int u,int v,int w)//链式前向星存图,无向则双向添加
{
e[cnt].v=v;//边的结尾节点
e[cnt].w=w;
e[cnt].next=head[u];//去找以u为起始的上一个节点(相当于链表,起始为-1)
head[u]=cnt++;//保存该边(最后的边)在e[i]中的编号
}
bool SPFA(int n,int x)//节点数量n,起点编号x
{
memset(dist,inf,sizeof(dist));
memset(vis,0,sizeof(vis));
memset(num,0,sizeof(num));
dist[x]=0;//该题起点任意
num[x]++;//入队次数++
queue<int> QAQ;
QAQ.push(x);
while(!QAQ.empty())
{
int now=QAQ.front();
QAQ.pop();
vis[now]=0;//从队列中取出
for(int i=head[now];i!=-1;i=e[i].next)
{//遍历以now开头的所有边,尝试以x->now->to松弛x->to
int to=e[i].v;//尝试松弛x->to的当前距离
if(dist[to]>dist[now]+e[i].w)
{
dist[to]=dist[now]+e[i].w;//成功用x->now->to松弛x->to
if(!vis[to])//x->to被成功松弛且to不在队列
{
vis[to]=1;//标记to加入队列
QAQ.push(to);
num[to]++;//to加入队列次数++
if(num[to]>n)
return 1;//有负权回路,无法求最短路径
}
}
}
}
return 0;
}
DFS优化(负环检测)

请先用前向星加边。因为图有可能不连通,检测负环要枚举每个起点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool spfa(int x)//DFS优化
{
vis[x]=1;
for(int i=head[x];~i;i=e[i].next){
int v=e[i].v;
if(dist[v]>dist[x]+e[i].w)//求最短路
{
dist[v]=dist[x]+e[i].w;
if(vis[v])//存在一点在一条路径上出现多次,存在负权环
return 0;
if(!spfa(v))//搜到了存在负权环
return 0;
}
}
vis[x]=0;
return 1;//未找到负权环
}

线段树优化建图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
struct edge
{
int v,w;
edge(){}
edge(int v,int w):
v(v),w(w){}
bool operator < (const edge &y)const
{
return w>y.w;
}
};
vector<edge>G[maxn];
inline void adde(int u,int v,int w)
{
G[u].push_back(edge(v,w));
}
//一棵树占4n的空间
int n,shift;
void build(int x,int l,int r)
{
if(l==r)
{//是叶子,向真正节点连边,双向0权
adde(x,l+2*shift,0);//合并出树叶子与原点
adde(l+2*shift,x,0);
adde(x+shift,l+2*shift,0);//合并点与入树叶子
adde(l+2*shift,x+shift,0);
return;
}
adde(x<<1,x,0);//出树,儿子向父亲建0权边,[l,r]->u
adde(x<<1|1,x,0);//右儿子->父
adde(x+shift,x*2+shift,0);//入树,父亲向儿子建0权边,u->[l,r]
adde(x+shift,x*2+1+shift,0);//父->右儿子
int mid=(l+r)>>1;
build(x<<1,l,mid);//左子树
build(x<<1|1,mid+1,r);//右子树
}
void add(int p,int l,int r,int nl,int nr,int x,int w,bool in)
{
if(nl>r||nr<l)
return;
if(l<=nl&&nr<=r)
{
if(in)
return adde(x+2*shift,p+shift,w);//入树,x->[l,r]
else
return adde(p,x+2*shift,w);//[l,r]->x;
}
int mid=(nl+nr)/2;
add(p<<1,l,r,nl,mid,x,w,in);
add(p<<1|1,l,r,mid+1,nr,x,w,in);
}
int dist[maxn];
void dijkstra(int s)
{
for(int i=1;i<=n+2*shift;i++)
dist[i]=INF;
vector<bool> vis(n+2*shift+1,false);
priority_queue<edge>q;
q.push(edge(s,0));
while(!q.empty())
{
edge now=q.top();
int x=now.v;
q.pop();
if(vis[x])
continue;
vis[x]=1;
dist[x]=now.w;
for(edge &i:G[x])
{
if(!vis[i.v]&&dist[i.v]>dist[x]+i.w)
{
dist[i.v]=dist[x]+i.w;
q.push(edge(i.v,dist[i.v]));
}
}
}
}
signed main(signed argc, char const *argv[])
{
int q,s,ope,u,v,w,l,r;
cin>>n>>q>>s;
shift=4*n;
build(1,1,n);//先对于in树和out树建边
for(int i=1;i<=q;i++)
{
cin>>ope;
if(ope==1)
{
cin>>u>>v>>w;
adde(u+2*shift,v+2*shift,w);
}
else if(ope==2)
{
cin>>u>>l>>r>>w;//x->[l,r]
add(1,l,r,1,n,u,w,1);
}
else{
cin>>u>>l>>r>>w;
add(1,l,r,1,n,u,w,0);
}
}
dijkstra(s+2*shift);
for(int i=1;i<=n;i++)
cout<<(dist[i+2*shift]==INF?-1:dist[i+2*shift])<<' ';
return 0;
}
/*
* https://www.luogu.com.cn/problem/CF786B
* n个点的有向带权图,有三种连有向边
* 1.u到v
* 2.u到点[l,r]都有一条边
* 3.[l,r]到v都有一条边
* 问从s出发的全局最短路
* 建图肯定不能暴力建图...
* https://www.luogu.com.cn/blog/_post/262486
*/

Floyd

1
2
3
4
5
6
7
8
9
10
11
long long path[805][805];
void floyd(int n)//节点编号1~n
{
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(path[i][k]+path[k][j]<path[i][j])
{//当i,j的原来的边的最短距离,大于经过k点所到达的距离就替换
path[i][j]=path[i][k]+path[k][j];
}
}

Floyd求最小环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int dp[maxn][maxn],mp[maxn][maxn];
signed main()
{
int n,m,u,v,d;
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dp[i][j]=mp[i][j]=(i==j?0:inf);
for(int i=1;i<=m;i++){
cin>>u>>v>>d;
dp[u][v]=dp[v][u]=mp[u][v]=mp[v][u]=min(dp[u][v],d);
}
int ans=inf;
for(int k=1;k<=n;k++){//i->k->j->i
for(int i=1;i<k;i++)//k为这个环上最大点,直接连接i与j
for(int j=i+1;j<k;j++)//dp[i][j]表示<i,j>只经过1~k-1优化的最短路
ans=min(ans,dp[i][j]+mp[i][k]+mp[k][j]);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)//如果经过k可以优化,那么它之前一定不经过k
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);
}
if(ans==inf) cout<<"No solution."<<endl;
else cout<<ans<<endl;
return 0;
}

最大团

最大团大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include<bits/stdc++.h>
using namespace std;
const int maxn=60;
int G[maxn][maxn],tmp[maxn][maxn];//tmp[i][j]搜到第i层
int n,ans,tot,f[maxn];//Bron-Kerbosch 算法,复杂度3^n
stack<int>stk,rec;
bool dfs(int dep,int num)//深度(团大小),备选集合大小num
{//当前层数dep,num表示与当前层相邻的比
if(num==0)
{
if(dep>ans)
{
ans=dep;
rec=stk;
return 1;
}
return 0;
}
for(int i=1;i<=num;i++)
{
if(dep+num-i+1<=ans)//剪枝1,不会超过当前最优解
return 0;
int v=tmp[dep][i];
if(dep+f[v]<=ans)//剪枝f[v]+已经取了的点仍不超过最优解
return 0;
int cnt=0;
for(int j=i+1;j<=num;j++)
{
int vv=tmp[dep][j];
if(G[v][vv])
tmp[dep+1][++cnt]=vv;
}
stk.push(v);
if(dfs(dep+1,cnt))
return 1;
stk.pop();
}
return 0;
}
int maxClique(int n)
{
if(n==0)
return 0;
memset(f,0,sizeof(f));
f[n]=ans=1;
for(int i=n-1;i>=1;i--)
{
tot=0;//以i为最小编号点的最大团
while(!stk.empty())
stk.pop();
stk.push(i);
for(int j=i+1;j<=n;j++)
if(G[i][j])
tmp[1][++tot]=j;
dfs(1,tot);
f[i]=ans;
}
return ans;
}
int main()
{//回溯法求最大团
scanf("%d",&n);
int u,v;
while(~scanf("%d%d",&u,&v))
G[u][v]=G[v][u]=1;
printf("%d\n",maxClique(n));
return 0;
}

差分约束

定义

如果一个系统是由n个变量和m个约束条件组成,并且每个约束条件能够形成形如$x_i−x_j\le c_k$的形式,我们就称该系统为差分约束系统。

解法

我们如果要求$x_n-x_1$的最大值,先将不等号方向统一,统一为$x_i−x_j\le c_k$形式时,从$x_j$向$x_i$连一条权值为$c_k$的边,跑从$x_1$到$x_n$的最短路,$dist(x_n)-dist(x_1)$即为所求答案。

如果求某两个变量的差的最小值(或者让一组解中最大值最小),那么将所有等式转换为$x_j−x_i\ge -c_k$,从$x_i$向$x_j$连一条权值为$-c_k$的边,跑最长路,$dist$数组即为此时的答案。

因为可能会出现负环,此时无解,所以要使用SPFA求解,建立一个超级源点并向所有点连一条$0$费用的单向边,最坏复杂度$O(nm)$。

例题

CF1131D Gourmet choice:留待研究,$x_i - x_j \ge c_k$的建图跑最长路。

CF1450E Capitalism:连通图上$n$个点,有$m$个关系,关系可以视为边,有如下两种。询问是否存在一种方案,使得存在关系的两个边的值不相等,若存在则输出使图上最大权值最大化的方案。

  • $x_u-x_v=1$(拆成$x_u-x_v\le 1$与$x_v-x_u\le -1$两条)

  • $|x_u-x_v|=1$(拆成$x_u-x_v\le 1$与$x_v-x_u\le 1$两条,并且枚举check不能相等)

思路:要使权值最大化,跑最短路,因为不确定以哪个点为起点能使最大距离最大,所以以每个点为起点都跑一遍并记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
vector<edge>G[maxn];
ll dist[maxn],ans=-inf,pos=0;
vector<pii>rec;
bool spfa(int n,int st)
{
vector<bool>vis(n+2,false);
vector<int>cnt(n+2,0);
queue<int>QAQ;
for(int i=1;i<=n;i++)
dist[i]=INF;
QAQ.push(st);
cnt[st]++;
dist[st]=0;
while(!QAQ.empty())
{
int u=QAQ.front();
QAQ.pop();
vis[u]=0;//出队
for(auto &e:G[u])
if(dist[e.v]>dist[u]+e.w)
{//最短路,因为要求最大值
dist[e.v]=dist[u]+e.w;
if(!vis[e.v])
{
if(++cnt[e.v]>n+1)
return true;
vis[e.v]=1;
QAQ.push(e.v);
}
}
for(pii &i:rec)//没有羡慕,无解
if(dist[i.ff]==dist[i.ss])
return true;
for(int i=1;i<=n;i++)
if(dist[i]>ans)
{
ans=dist[i];
pos=st;
}
return false;
}
signed main(signed argc, char const *argv[])
{
int n,m,u,v,w;
cin>>n>>m;
for(int i=1;i<=m;i++)
{
cin>>u>>v>>w;
if(w==1)//u羡慕v,x_v=x_u+1
{
G[u].push_back(edge(v,1));
G[v].push_back(edge(u,-1));
}
else{//不知道谁羡慕谁
G[u].push_back(edge(v,1));
G[v].push_back(edge(u,1));
rec.push_back(pii(u,v));
}
}
for(int i=1;i<=n;i++)
if(spfa(n,i))
return cout<<"NO"<<endl,0;
spfa(n,pos);
cout<<"YES"<<endl;
cout<<ans<<endl;
for(int i=1;i<=n;i++)
cout<<dist[i]<<' ';
return 0;
}

最小生成树(MST)

Prim算法

复杂度:邻接矩阵:$O(V^2)$,邻接表:$O(ElogV)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const int inf=0x3f3f3f3f,maxn=105;
int dist[maxn][maxn],closest[maxn],lowcost[maxn];
bool tree[maxn];
int prim(int n,int u0)
{
memset(tree,0,sizeof(tree));
tree[u0]=1;//加入树
int i,j;
for(i=1;i<=n;i++)//① 初始化,
if(i!=u0){
lowcost[i]=dist[u0][i];
closest[i]=u0;//一开始只有u0
}
else
lowcost[u0]=0;
for(i=1;i<=n;i++)//② 每次选出来一个最近的节点
{
int temp=inf,t;
for(j=1;j<=n;j++)//③ 在V-u中寻找最近的节点t
{
if(!tree[j]&&lowcost[j]<temp)
{//在剩下的节点找一个最近的
t=j;
temp=lowcost[j];
}
}
if(t==u0)//找不到t,没有可加入的节点,跳出
break;
tree[t]=1;//找到了就加入tree
for(j=1;j<=n;j++)//④ 根据加入的t节点更新lowcost和closest
{
if(!tree[j]&&dist[t][j]<lowcost[j])
{//对于剩下的节点维护
lowcost[j]=dist[t][j];
closest[j]=t;
}
}
}
int ans=0;
for(i=1;i<=n;i++)
ans+=lowcost[i];
return ans;
}

Kruskal

使用并查集优化,复杂度$O(mlogm)$

这个不能忘吧……就先不贴了

Boruvka

因为没有$kruskal$好写,所以一般不用于MST裸题。

适于处理边权由连接的两个点的点权通过某种计算方式得出的情况。

平均 $O(V+E)$,最坏 $O((V+E)logV)$。

流程
  1. 对每个连通块,处理出与其他连通块连接的最小代价,并记录这条边。

  2. 连接所有连通块与其最小连接代价的连通块,并将该边边权计入。

  3. 若剩余连通块数量大于1,重复上述步骤。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
struct edge{
int u,v,w;
};
vector<edge> E;
int boruvka(int n)
{
for(int i=1;i<=n;i++)//n个连通块
fa[i]=i;
int ans=0;
vector<int> cost(n+1),rec(n+1);
vector<bool> vis(E.size(),false);
while(1)
{
for(int i=1;i<=n;i++)
cost[i]=inf;//初始化为inf
int cur=0;//统计不同连通块
for(int i=0;i<E.size();i++)
{
int a=findfa(E[i].u),b=findfa(E[i].v),w=E[i].w;
if(a==b)
continue;
cur++;//记录a,b两个连通块连接的最小代价
if(w<cost[a])
cost[a]=w,rec[a]=i;//记录最小联通代价与相应边
if(w<cost[b])
cost[b]=w,rec[b]=i;
}
if(cur==0)
break;
for(int i=1;i<=n;i++)
{//最坏情况是连接的连通块数目/2
if(cost[i]<inf&&!vis[rec[i]])//与i相接的权值最小的边未加入
{
Merge(E[rec[i]].u,E[rec[i]].v);//连接两个连通块
ans+=E[rec[i]].w;
vis[rec[i]]=true;//标记该边已加入,避免重复计算
}
}
}
return ans;
}

典型例题

  • 打井问题 :将地下水源缩成一个点,添加到图中,建立MST。

  • 无根MDST:建立超级源点s,向每一个节点连接一条值为INF的边,以s为根跑MDST,s的出边即为答案MDST的树根。

  • 异或生成树:字典树合并连通块

CF888G Xor-MST 异或生成树

题意

$n$节点无向完全图,每个点有点权$a_i$,连接点$i$与点$j$的边的边权为$a_i \oplus a_j$,求这个图的MST的权值,$1\le n \le 2\times 10^5, 0\le a_i \le 2^{30}$。

思路

异或最值问题,套一个trie,如果对这个套路不熟悉,请先做CF923C Perfect Security
虽然已经没了Borůvka的形但是思想还在。
首先我们对所有点权排序并去重,因为相同的值异或为0,在图中必然是这些点相连且没有贡献,所以我们不用考虑这部分。
将剩下的点权插入trie,从根节点到叶子的一条路径就代表一个点权,叶子结点的数量就代表点权的数量。
初始时每个叶子都代表一个连通块,求解这个问题就是不断连接这些连通块的过程。
对于trie上的节点$x$,如果其左右子树都存在,那么就必须找一条边连接其左右子树,若$x$的所处位数为$w$,则这条边必有$1<<w$的贡献,对于$w$之前的位数,因为$x$到根节点路径相同异或值都为0;对于$[w-1,0]$的位数,这部分的贡献要对trie进行搜索来得到。
此时$x$左右子树已经联通,回溯继续向上处理即可。
$check$最坏情况下一次遍历整个trie树,$O(nlogn)$,最多合并$nlogn$次。
整体时间复杂度$O(nlog^2n)$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf=0x3f3f3f3f,mod=1000000007;
const int maxn = 200005, M = 2;
int val[maxn],trie[maxn<<5][M],siz[maxn<<5],tot;
void init()
{
tot=1;
}
void Insert(const int key)//rt传入0
{//trie插入模式串
int rt=0;
for(int i=30;i>=0;i--)
{
int id = (key>>i)&1;
if(!trie[rt][id])
trie[rt][id]=tot++;
rt = trie[rt][id];
}
}
ll check(int x,int y,int w)//启发式,最多向下2^w次?,但最多1e5个节点
{//在x子树和y子树上求得一个最小异或值
ll ret=LLONG_MAX;
if(trie[x][0]&&trie[y][0])//尽量先使高位相同
ret=min(ret,check(trie[x][0],trie[y][0],w-1));
if(trie[x][1]&&trie[y][1])
ret=min(ret,check(trie[x][1],trie[y][1],w-1));
if(ret==LLONG_MAX)
{
if(trie[x][0]&&trie[y][1])//w位此时一定为1了
ret=min(ret,check(trie[x][0],trie[y][1],w-1))+(1<<w);
if(trie[x][1]&&trie[y][0])
ret=min(ret,check(trie[x][1],trie[y][0],w-1))+(1<<w);
if(ret==LLONG_MAX)//说明两边没有节点
return 0;
}
return ret;
}
ll dfs(int x,int w)//节点x开始搜索,右数第w位
{//从高位向下dfs
if(w<0)//只有根节点为0
return 0;
if(trie[x][0]&&trie[x][1])//都存在,找最小边连接两个连通块
return (1<<w)+check(trie[x][0],trie[x][1],w-1)+dfs(trie[x][0],w-1)+dfs(trie[x][1],w-1);
else if(trie[x][0])//左面存在
return dfs(trie[x][0],w-1);
else//右面存在
return dfs(trie[x][1],w-1);
}
signed main(int argc, char const *argv[])
{
int n,ans=0;
init();
cin>>n;
for(int i=1;i<=n;i++)
cin>>val[i];
sort(val+1,val+n+1);
n=unique(val+1,val+n+1)-val-1;//去重是因为相同值异或为0,没必要再计算
for(int i=1;i<=n;i++)
Insert(val[i]);
cout<<dfs(0,30)<<endl;
return 0;
}

次小生成树

严格次小生成树(洛谷P4180)

建立最小生成树MST,倍增维护MST上任一点到LCA最大值与次大值。

记录树的重量val,并对边集中选中边进行标记。倍增算出每个节点到祖先的路径最大边与严格次大边权值。枚举每一条不在MST上的的边u->v(权值w),分别计算新树上u->lca(u,v)与v->lca(u,v)的路径最大边权M严格次大边权m,记录最小非零增量inc=w-M(当M==w时inc=w-m)。最后val+inc即为次小生成树重量。

复杂度瓶颈在排序,$O(mlogm)$

有向图最小生成树(MDST,最小树形图)

  • 树形图:有向图$G=(V,E)$中,选定根节点$root$,$G$的一个以$root$为根节点的子图$T$,$T$中$root$到任意其他节点路径存在且唯一。则$T$称为有向图生成树/树形图DST。

  • 最小树形图:带权有向图$G=(V,E,w)$中边权总和最小的DST,即Minimum Directed Spanning Trees。

  • 特点:

    • MDST上一定有且仅有$n-1$条边
    • 根节点入度为0,其他节点入度为1

朱刘算法(Edmonds 算法)

流程
  1. 对每个非根节点,找出权值最小的入边(n-1条),记为集合$E$。若$E$不存在,则MDST一定不存在。
  2. 判断E中是否成环,成环转到步骤3,否则转到步骤4。
  3. 若成环则进行缩点,同时更新指向该环的所有边的权值,此更新等效于删去环上的一条边。
    • 记该环为$C$,在新图$G_1$中收缩为点$u$,则对于在$G$图中不在环上的且指向该环上任一点$v$的一条边$<v_1,v>$,该边权值记为$w$,在$G_1$中存在边$<v_1,u>$与之对应,且该边权值$W_{G_1}(<v_1,u>)$=$W_{G}(<v_1,v>)-w$。
    • 因为任何一个节点入度都不会大于1,在环$C$上已经为点$v$选择了一条入边,所以要根据改边权值更新其他点$v$入边权值,当接下来选择了其他指向$v$的边时,相当于删去了$C$上指向$v$的边。
    • 转到步骤1,直到证明不存在或者求得。
  4. 不成环则展开收缩点,获得MDST。若仅须获得MDST的权值,则不需要展开。
模板

朱刘算法,不包括展开部分,未优化,复杂度$O(VE)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1005,inf=0x3f3f3f3f;
struct edge
{
int u,v,w;
edge(int u,int v,int w):
u(u),v(v),w(w){}
};
vector<edge> G;//该算法会修改边
int id[maxn],in[maxn],pre[maxn],vis[maxn];//in[x]表示x入边最小权,pre[x]表示x最小入边的出点
ll zltree(int root,int n)//id[x]为x节点在G图上的编号
{
ll ans=0;
while(1)
{
fill(in,in+n+1,inf);
for(auto &e:G)
if(e.u!=e.v&&e.w<in[e.v])
{
pre[e.v]=e.u;//记录最小入边出点
in[e.v]=e.w;//记录最小入边权
}
for(int i=1;i<=n;i++)
if(i!=root&&in[i]==inf)
return -1;//存在非根点没有入边,无MDST
fill(id,id+n+1,0);
memset(vis,0,sizeof(vis));
int tn=0,v;//tn记录环的数量
in[root]=0;//根节点无入边,权为0(这样不用特判)
for(int i=1;i<=n;i++)//找环
{
ans+=in[v=i];//加v入边贡献
while(vis[v]!=i&&!id[v]&&v!=root)//
vis[v]=i,v=pre[v];//检查v的最小入边出点,并标记vis为i
if(v!=root&&!id[v])
{
id[v]=++tn;//标记环的编号
for(int u=pre[v];u!=v;u=pre[u])
id[u]=tn;//将v所在环打上同一个标记
}
}
if(tn==0)//无环
break;
for(int i=1;i<=n;i++)
if(!id[i])//给不在环上的点新编号
id[i]=++tn;
for(int i=0;i<(int)G.size();)//更新为新图G1
{
auto &e=G[i];
v=e.v;
e.u=id[e.u],e.v=id[e.v];
if(e.u!=e.v)//更新指向环的边权
e.w-=in[v],i++;
else
{
swap(e,G.back());
G.pop_back();
}
}
n=tn;//更新新图的点数
root=id[root];//更新新图上根节点编号
}
return ans;
}
int main()
{
int n,m,r,u,v,w;
cin>>n>>m>>r;//n节点,m条边,根节点r
for(int i=1;i<=m;i++)
{
cin>>u>>v>>w;
G.push_back(edge(u,v,w));//有向
}
cout<<zltree(r,n)<<endl;
return 0;
}

kruskal重构树

用于求解无向有权图上任意两点$u$到$v$间选择一条路线,使最大边最小化,求该最大边的值。

该值即为$kruskal$重构树上$u$与$v$节点的权值,最小化最大边求值同理。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for(int i=1;i<=2*n;i++)//一定要初始化2n
fa[i]=i;
int tot=n;
for(auto &e:E)
{//E为排好序的边集
int a=findfa(e.u),b=findfa(e.v);
if(a==b)
continue;
fa[a]=fa[b]=++tot;//合并并查集
val[tot]=e.w;//新lca的权值
G[tot].push_back(a);//新节点指向原来并查集的根
G[tot].push_back(b);
}
init(tot,tot);//处理重构树lca
cin>>q;
while(q--)
{
cin>>u>>v;
cout<<val[lca(u,v)]<<'\n';
}

矩阵树定理

无向图$G$的生成树个数等于其基尔霍夫矩阵任何一个$n-1$阶主子式的行列式的绝对值(或者任意一个1阶子式的代数余子式)。

基尔霍夫矩阵$K$=度数矩阵$D$−邻接矩阵$A$。
$$
D_{ij}=\begin{cases}deg(v_i)&,v_i=v_j\0&,v_i\ne v_j \end{cases},A_{ij}=\begin{cases}1&,v_i与v_j相接\0&,v_i不与v_j相接 \end{cases},K_{ij}= \begin{cases}deg(v_i)&,i=j\-1&,v_i与v_j相接\0&,v_i与v_j不相接\end{cases}
$$

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ll Kirchhoff(int n)
{//传入基尔霍夫矩阵及其阶数
n--;//删去最后一行和最后一列
ll ans=1;
for(int i=1;i<=n;i++)//高斯消元求解行列式
{
for(int j=i+1;j<=n;j++)
while(Kir[j][i])
{
ll tmp=Kir[i][i]/Kir[j][i];
for(int k=i;k<=n;k++)
{
Kir[i][k]=(Kir[i][k]-tmp*Kir[j][k]%mod+mod)%mod;
std::swap(Kir[j][k],Kir[i][k]);
}
ans=-ans;
}
ans=(ans*Kir[i][i])%mod;
}
return (ans+mod)%mod;//得到生成树个数
}

扩展

外向树:所有边的方向都是从根指向叶子。
内向树:所有边的方向都是从叶子指向根。

  1. 内向生成树个数:将度数矩阵换成点的入度。
  2. 外向生成树个数:将度数矩阵换成点的出度。

虚树

虚树是在树形dp中使用的一种特殊优化,适用于树中仅有少量关键节点且普通节点很多的情况。可以将关键点和他们的LCA拿出来另建一棵树,并在这棵树上另外进行树形dp。

步骤

  1. 在原树上进行dfs,进行LCA预处理,同时得到原树上的dfs序,方便之后虚树构造,此外还可以进行一些dp预处理,便于进行虚树上的第二次dp。
  2. 确定关键节点集合,并按照dfs序排序。
  3. 通过单调栈及LCA算法构建出虚树。
  4. 在虚树上进行树形dp求解。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
sort(h+1,h+k+1,[](const int &x,const int &y){
return dfn[x]<dfn[y];//按dfs序排序
});
stk[top=1]=1;//指定1为根,否则可以指定h[1]
g[1].clear();//奇怪的清空优化1
for(int i=1;i<=k;i++)//k个重要节点
{
int now=h[i];
int lc=lca(now,stk[top]);//最近公共祖先
//printf("lca(%d,%d)=%d\n",now,stk[top],lc);
while(top>1&&dfn[lc]<=dfn[stk[top-1]])//情况4,=是情况3
{//不断将top送入虚树
adde(stk[top-1],stk[top]);//前向星加边,构建新树
top--;
}
if(dfn[lc]<dfn[stk[top]])//情况2
{//加边,top出栈,lc和now入栈
g[lc].clear();//奇怪的清空优化2
adde(lc,stk[top]);
stk[top]=lc;
}//否则为情况1
g[now].clear();//奇怪的清空优化3
stk[++top]=now;
}
while(--top)
adde(stk[top],stk[top+1]);

例题

拓扑排序

只适用于DAG。

使用队列(要求字典序时使用优先队列),在邻接表存边时统计每个结点的入度,入度为0则入队。按出队顺序编号,删除以该节点为尾的边,该边边头入度减1,若变为0则入队。直到队列为空,得到top数组与node数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const int maxn=105;
vector<int> G[maxn];
int n,m,top[maxn],node[maxn];//节点i拓扑顺序为top[i]
void topsort(void)
{//要求字典序:优先队列,小根堆
priority_queue<int,vector<int>,greater<int> > QAQ;//若要求字典序用优先队列
for(int i=1;i<=n;i++)
if(G[i][0]==0)//G[i][0]表示入度
QAQ.push(i);
int cnt=0;
while(!QAQ.empty())
{
int x=QAQ.top();
top[x]=++cnt;//top[x]表示x号节点的次序编号
QAQ.pop();
for(int i=1;i<G[x].size();i++)
{
if(--G[G[x][i]][0]==0)//节点入度为0时入队
QAQ.push(G[x][i]);
}
}
if(cnt!=n)//
return;
for(int i=1;i<=n;i++)
node[top[i]]=i;//node[i]表示排序为i的节点为node[i]
// for(int i=1;i<=n;i++)
// printf("%d%c",node[i],i<n?' ':'\n');//输出拓扑排序后的节点
return;
}
int main()
{
while(cin>>n>>m&&(n||m)){
for(int i=1;i<=n;i++){
G[i].clear();//注意清空
G[i].push_back(0);//G[i][0]用来统计节点i的入度
}
for(int i=0;i<m;i++){
int u,v;
cin>>u>>v;
G[u].push_back(v);//向G中加边
G[v][0]++;//v的入度+1
}
topsort();
}
return 0;
}

补图搜索

也是很套路的题了,使用$set$数组来代替邻接表,用$set.find(v)$来确定边是否在图中。并查集、DFS、BFS都可以做。

补图连通块 0-1 MST

初始化$ans=0$,并将所有的点放进一个未访问集合$unvis$中,当集合非空时,取出$unvis.begin()$记为$now$并从集合中去掉,并从该点开始BFS,遍历$nuvis$集,并在邻接表$set[now]$中查询,若$set.find()$未找到,则说明该边为补边。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
set<int> G[maxn];//使用set存原图,加快速度
int bfs(int n)
{
int ans=0;
set<int> unvis;//已访问节点,从原图上删除
for(int i=1;i<=n;i++)
unvis.insert(i);//将所有点加入到nuvis集合中
while(!unvis.empty())//BFS求连通块个数
{
ans++;//从未访问集里拿出新的元素,新增一个连通块
int now=*(unvis.begin());//取出第一个
unvis.erase(now);
queue<int> QwQ;
QwQ.push(now);
while(!QwQ.empty())//找出与now联通的节点,并从unvis中删去
{
int nex=QwQ.front();//与now联通的点之一
QwQ.pop();
vector<int> v1;//记录要删去的节点
for(auto i:unvis)//遍历未访问集合
if(G[nex].find(i)==G[nex].end())
{//now与i由补边连接,权重为0
v1.push_back(i);
QwQ.push(i);//放进队列里,继续向下求联通点
}
for(int i=0;i<v1.size();i++)
unvis.erase(v1[i]);//在集合中删去搜到的联通节点
}
}
return ans;
}

欧拉路

给定一张联通无向/有向图,求一条经过所有边恰好一次的回路。

有解当且仅当所有点 度数为偶数(无向)/**入度等于出度(有向)**。 任选一点开始dfs,每条边只经过一次。回溯时将回溯的边加入队列,最后队列的逆序就是答案。 时间复杂度$O(m)$。
欧拉路径也可以用一样的方法求出(找度数为奇数的点进行DFS)。

判定

  • 无向图欧拉回路:所有点度数都为偶数
  • 有向图欧拉回路:所有点入度=出度
  • 无向图欧拉路:有0或2个点度数为奇数,其余点度数为偶数
  • 有向图欧拉路:一个点入度=出度+1,一个点出度=入度+1,其余点入度=出度

Hierholzer算法

待补…

两笔画问题

有解当且仅当入度为奇数的点不超过四个。 将其中两个点加一条边后求欧拉路径,然后在这条边处断开成两条欧拉路即可。 时间复杂度$O(m)$。

汉密尔顿回路

求简单无向图的环数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const int maxn=19,inf=0x3f3f3f3f,mod=1000000007;
ll dp[1ll<<maxn][maxn],ans;
vector<int>G[maxn];
signed main(signed argc, char const *argv[])
{
int n,m,u,v;
cin>>n>>m;
for(int i=1;i<=m;i++)
{
cin>>u>>v;
u--,v--;//现在是[0,18]了
G[u].push_back(v);
G[v].push_back(u);
}
//dp[k][x]表示当前点为x,前面经过点的状态为k的简单路径条数
//且经过的第一个点是k二进制下第一位
for(int i=0;i<n;i++)
dp[1ll<<i][i]=1;
#define lowbit(x) ((x) & -(x))
for(int k=1;k<(1ll<<n);k++)
{//k为经过点的状态
for(int x=0;x<n;x++)
{
if(!dp[k][x])
continue;
for(int &v:G[x])
{//枚举x相邻点v
if(lowbit(k)>(1ll<<v))//v小于经过第一个点,非法
continue;
if(k&(1ll<<v))//x是已经过点
{
if(lowbit(k)==(1ll<<v))//是最早经过点,成环
ans+=dp[k][x];
}
else//未经过点,更新状态
dp[k|(1ll<<v)][v]+=dp[k][x];
}
}
}
cout<<(ans-m)/2<<endl;
return 0;
}

强连通分量

Tarjan

Tarjan缩点+DAG 拓扑排序dp

n个点m条边的有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。

思路:使用Tarjan缩点建立新图DAG,在DAG上进行拓扑排序并进行DP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
const int maxn=100005;
int tot=0,Index=0,cnt=0;
int stk[maxn],dfn[maxn],low[maxn],belong[maxn];
int val[maxn],rec[maxn];//每个强连通分量的值
bool vis[maxn];
vector<int> G[maxn];//邻接表
void tarjan(int x)//标准的Tarjan缩点
{
dfn[x]=low[x]=++tim;//dfs序
stk[++Index]=x;
vis[x]=1;
for(int &v:G[x])
{
if(!dfn[v])//v未被访问
{
tarjan(v);
low[x]=min(low[x],low[v]);//回溯时更新low
}//low[x]为x所在强连通分量最早起始节点
else if(vis[v])//v在栈中,说明有环
low[x]=min(low[x],dfn[v]);//更新起点为最早的那个
}
if(low[x]==dfn[x])
{//以x为起点的强连通分量
cnt++;//新图节点++
do{
belong[stk[Index]]=cnt;
rec[cnt]+=val[stk[Index]];//缩点后的权值
vis[stk[Index]]=0;
Index--;
}while(stk[Index+1]!=x);
}
}
vector<int> Gra[maxn];
int dp[maxn],top[maxn],into[maxn];
int topsort(void)
{
queue<int> QAQ;
for(int i=1;i<=cnt;i++)
{
if(!into[i])
QAQ.push(i);
dp[i]=rec[i];
}
int flag=0;
while(!QAQ.empty())
{
int x=QAQ.front();
QAQ.pop();
top[x]=++flag;
for(int i=0;i<Gra[x].size();i++)
{
dp[Gra[x][i]]=max(dp[Gra[x][i]],dp[x]+rec[Gra[x][i]]);
if(--into[Gra[x][i]]==0)
QAQ.push(Gra[x][i]);
}
}
int ans=0;
for(int i=1;i<=cnt;i++)
ans=max(ans,dp[i]);
return ans;
}
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>val[i];//每个点的值
for(int i=0;i<m;i++)
{
int u,v;
cin>>u>>v;
G[u].push_back(v);
}
for(int i=1;i<=n;i++)//Tarjan缩点部分
if(!dfn[i])
tarjan(i);//缩点
for(int i=1;i<=m;i++)//拓扑排序部分
for(int j=0;j<G[i].size();j++)
{
int x=belong[i],y=belong[G[i][j]];
if(x!=y)
{
Gra[x].push_back(y);//建立新图DAG
into[y]++;
}
}
cout<<topsort()<<endl;
return 0;
}
双连通分量(biconnected component)

双连通分量概念: 双连通分量有点双连通分量和边双连通分量两种。若一个无向图中的去掉任意一个节点(一条边)都不会改变此图的连通性,即不存在割。

  • 割点:去掉这个点,原无向联通图不再联通。

  • 割边/桥:去掉这条边,原无向联通图不再联通。

  • 点双连通:删掉一个之后,图仍联通

  • 边双连通:删掉一条之后,图仍联通

点双连通:在一个无向图中,若任意两点间至少存在两条“点不重复”的路径,则说这个图是点双连通的。点双连通的极大子图称为点双连通分量(简称双连通分量,Biconnected Component,BCC)。

点双连通图不存在割点的图,边双连通图即不存在的图。

  • BCC中无割点,任意两点间至少存在两条点不重复的路径等价于图中删去任意一个点都不会改变图的连通性
  • 若BCC间有公共点,则公共点为原图的割点
  • 无向连通图中割点一定属于至少两个BCC,非割点只属于一个BCC
点双连通分量缩点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int dfn[maxn],low[maxn],stk[maxn],belong[maxn],index=0,cnt=0,tim=0;
vector<int>G[maxn],g[maxn];
bool vis[maxn];
void tarjan(int x,int fa)
{
low[x]=dfn[x]=++tim;
vis[x]=1;//标记在栈中
stk[++index]=x;
for(int &v:G[x])
{
if(v==fa)
continue;
if(!vis[v])
{
tarjan(v,x);
low[x]=min(low[x],low[v]);
}
else
low[x]=min(low[x],dfn[v]);
}
if(low[x]==dfn[x]&&vis[x])
{
cnt++;//双连通分量个数
do{
belong[stk[index]]=cnt;
vis[stk[index]]=0;
index--;
}while(stk[index+1]!=x);
}
}
Tarjan无向图求割点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int tot=0,Index=0,ans=0,cnt=0;
int dfn[maxn],low[maxn],stk[maxn];
bool vis[maxn],isc[maxn];
void tarjan(int x,int fa)
{
dfn[x]=low[x]=++tot;
int child=0;
for(int i=0;i<G[x].size();i++)
{
int v=G[x][i];
if(!dfn[v])
{
tarjan(v,x);
low[x]=min(low[x],low[v]);
if(x==fa)
child++;
if(x!=fa&&low[v]>=dfn[x])
isc[x]=1;
}
else if(v!=fa)//不同之处
low[x]=min(low[x],dfn[v]);
}
if(x==fa&&child>=2)
isc[x]=1;
}
无向图求桥
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void tarjan(int x,int fa)
{//
dfn[x]=low[x]=++tot;
bool vis=0;//处理重边要加上,表示这个节点还没有被子树搜到
for(int i=0;i<G[x].size();i++)
{
int v=G[x][i].v,no=G[x][i].no;
if(!dfn[v])
{
tarjan(v,x);
if(low[v]>dfn[x])//讨论桥是大于
{
bri[no]=1;//法1,对桥的编号做标记
// pair<int,int> tem;//法二,将桥存到新的数组中
// tem.first=x,tem.second=v;
// ans[flag++]=tem;
}
low[x]=min(low[x],low[v]);
}
else if(dfn[x]>dfn[v])//可改为无条件?
{
if(v==fa&&!vis)
vis=1;//除了第一次,每次回到父节点都用父节点的值更新当前结点的值
else//之前是v!=fa时才用父节点值更新该点的值
low[x]=min(low[x],dfn[v]);
}
}
}

2-SAT问题

流程
  1. 将点$i$拆成$i$与$n+i$两个点,分别表示点$i$状态为$0$或$1$,二者必须且只能取其一。
  2. 根据所给逻辑关系建图,将$2n$个点进行缩点。
  3. 若存在一对拆点位于同一个强连通分量,则无解。
  4. 否则对于每个点对,选择分量编号较小的点(即拓扑序较大的那个)。
建图方式
逻辑表达式 连接的有向边(推导关系)
$a=1$,$a$单个点必为真,为假同理 $\lnot a \rightarrow a$
$a\land b=1$ $\lnot a\rightarrow a,\lnot b \rightarrow b$
$a\land b=0$ $a\rightarrow \lnot b,b \rightarrow \lnot a$
$a \lor b=1$ $\lnot a\rightarrow b,\lnot b \rightarrow a$
$a \lor b=0$ $a\rightarrow \lnot a,b \rightarrow \lnot b$
$a\oplus b=1$ $\lnot a\rightarrow b,b \rightarrow \lnot a,\lnot b \rightarrow a,a\rightarrow \lnot b$
$a\oplus b=0$,或$a\odot b=1$ $a\rightarrow b,b\rightarrow a,\lnot a\rightarrow \lnot b,\lnot b\rightarrow \lnot a$
$a\rightarrow b$,$\lnot a \lor b$,为假当且仅当$a$为真$b$为假 $a\rightarrow b,\lnot b\rightarrow\lnot a$
$\lnot a\rightarrow\lnot b,a\lor \lnot b$ $\lnot a\rightarrow\lnot b,b\rightarrow a$

圆方树

圆方树就是一种将图变成树的方法,可以处理仙人掌(每条在不超过一个简单环中的无向图)。

顶点仙人掌:每个点不会在超过一个简单环中的联通无向图,其特点为缩点后每个点不会在超过两个BCC中。

在圆方树中,原来的每个对应一个圆点,每一个点双对应一个方点。所以共有 $n+c$ 个点,其中 $n$ 是原图点数,$c$ 是原图点双连通分量的个数。如果原图有 $k$ 个连通分量,则它的圆方树也会形成 $k$ 棵树形成的森林。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
namespace rst{
int siz[maxn<<1],dfn[maxn],low[maxn],dfc=0,stk[maxn],top=0;
int belong[maxn<<1],cnt=0,n;//方点的个数
vector<int>pg[maxn],G[maxn<<1];
void tarjan(int x)
{//点双缩点并建立圆方树
dfn[x]=low[x]=++dfc;//初次访问x
fprintf(stderr,"Enter: #%d,dfn=%d\n",x,dfn[x]);
stk[++top]=x;//入栈
for(auto &v:pg[x])
{
if(!dfn[v])
{
tarjan(v);
low[x]=std::min(low[x],low[v]);//未访问的和low
if(low[v]==dfn[x])
{
cnt++;//增加方点个数/BCC个数
fprintf(stderr,"\nFind a new bcc #%d.\n",cnt-n);
do{//点双中所有点向方点连边,除了x都退栈
G[cnt].push_back(stk[top]);//
G[stk[top]].push_back(cnt);
siz[cnt]++;
fprintf(stderr,"Bcc #%d has vertex #%d.\n",cnt-n,stk[top]);
top--;
}while(stk[top+1]!=v);
siz[cnt]++;
G[cnt].push_back(x);//x自己连边,但不退栈
G[x].push_back(cnt);//因为一个点可能在多个bcc中
fprintf(stderr,"Bcc #%d has top vertex #%d\nsize = %d.\n",cnt-n,x,siz[cnt]);
}
}
else
low[x]=std::min(low[x],dfn[v]);//已访问的和dfn
}
fprintf(stderr,"");
}
void build(int n)
{
rst::n=cnt=n;//初始有n个点
top=dfc=0;
for(int i=1;i<=n;i++)
if(!dfn[i])//tarjan退出时根仍在栈内
tarjan(i),top--;
}
}// namespace rst

最近公共祖先(LCA)

因为LCA只适用于树,所以经常和生成树在一起考。

倍增

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const int maxn=100005;
const int maxl=30;
vector<int> G[maxn];//无权边,也可以选择链式前向星存图
int gene[maxn][maxl],depth[maxn],lg[maxn];
int nodes[maxn];//子树结点的数量
void dfs(int x,int fa)
{
depth[x]=depth[fa]+1;
gene[x][0]=fa;
nodes[x]=1;
for(int i=1;(1<<i)<=depth[x];i++)//倍增
gene[x][i]=gene[gene[x][i-1]][i-1];
for(int i=0;i<G[x].size();i++)
if(G[x][i]!=fa)
{
dfs(G[x][i],x);//在dfs前后加语句可以求出许多有趣的东西
nodes[x]+=nodes[G[x][i]];
}
}
int lca(int x,int y)
{
if(depth[x]<depth[y])//保证x深度≥y
swap(x,y);
while(depth[x]>depth[y])//将x提到同一高度
x=gene[x][lg[depth[x]-depth[y]-1]];
if(x==y)//是同一个节点
return x;
for(int i=lg[depth[x]];i>=0;i--)
if(gene[x][i]!=gene[y][i])
{//二分思想,直到跳到LCA的下面一层
x=gene[x][i];
y=gene[y][i];
}
return gene[x][0];
}
int dist(int x,int y)//x节点到y结点的距离
{
int tem=lca(x,y);
return (int)(abs(depth[x]-depth[tem])+abs(depth[y]-depth[tem]));
}
void init(int s,int n)
{
memset(nodes,0,sizeof(nodes));
// memset(gene,0,sizeof(gene));
depth[s]=1;
for(int i=1;i<=n;i++)//预处理出log2(i)+1的值
lg[i]=lg[i-1]+((1<<(lg[i-1]+1))==i);//不要写错
dfs(s,0);
}
应用
  • 次小生成树:P4180严格次小生成树

  • 树上两条路径是否相交:仓鼠找sugar

    1
    2
    3
    u=lca(a,b),v=lca(c,d);
    if(dist(u,c)+dist(u,d)==dist(c,d)||dist(v,a)+dist(v,b)==dist(a,b))
    cout<<"Yes"<<endl;
  • 给定节点,求以它为LCA的节点有多少种组合:找祖先 ,DFS回溯求出

  • 分类讨论求树上到任意两点距离相等的点的个数(重点讨论中节点与LCA关系):A and B and Lecture Rooms

  • 求两节点路径上最大/最小边的权值,若求最小即为求容量:货车运输 ,DFS时倍增求出路径上min

树上差分

用于求解一些树上路径问题。

常见问法:将一棵树上从u到v路径上的点/边的权值加上x,询问某点/边的权值。

$O(1)$修改,$O(n)$查询,复杂度决定要离线。

强行在线可用树链剖分$O(\log n \times \log n)$修改,$O( \log n \times \log n)$查询。

修改差分数组之前先用DFS倍增求出各节点LCA。

差分数组记为power[maxn],直接修改即可,查询时调用DFS

性质

  • 树上任意两点之间有且只有一条路径
  • 一个节点只有一个父节点
  • $x$节点到$y$结点的路线为:$x→lca(x,y)→y$

点的差分

操作

每次修改使$u$到$v$的路径上所有节点权值+1(包括端点),询问某一节点权值。

修改

$power[u]++,\ power[v]++,\ power[lca(u,v)]–,\ power[father[lca(u,v)]]–$

边的差分

操作

以点代边,$power[x]$代表$x$节点到父节点的边的权值。

若查询边的权值,则需要按输入顺序对每条边进行编号。

每次修改使节点$u$与节点$v$之间所有边权值+1,询问某一条边权值。

修改

$power[u]++,\ power[v]++,\ power[lca(u,v)]-=2$

查询

1
2
3
4
5
6
7
8
9
10
11
int power[maxn];//power[x]即为x节点权值
void dfs(int x)//查询,求出所有节点权值
{
for(int i=head[x];~i;i=G[i].nex)//枚举x所有子节点
if(G[i].v!=gene[x][0])//不为x父节点
{
dfs(G[i].v);//
power[x]+=power[G[i].v];
}
ans=max(ans,power[x]);
}

树链剖分

注意以点代边的思想

  • 功能
    1. 更新/查询某个节点子树的权值
    2. 更新/查询树上两个节点间所有点的权值
  • 性质
    1. 子树的时间戳一定全部小于父节点,并且连续(所以可用线段树维护)
    2. 任何一条路径都是由重链的一部分与重链间的叶子节点构成
    3. 任何父节点都一定在一条重链上(所以可用top的父节点跳链)
  • 例题
    1. P3384 树链剖分

通用模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
int n,tot=0,head[maxn];
double val[maxn];//给定的每个节点的权值
struct edge
{//边权一般不必记录到这里
int v,nex;
ll w;
} e[maxn<<1];
inline void add(int u,int v,ll w=0)
{
e[++tot].v=v;
e[tot].w=w;
e[tot].nex=head[u];
head[u]=tot;
}
//以点代边:以节点的权值代表该节点到父节点边的权值,修改与查询跳过链顶点即可(最终的参数改为dfn[x]+1)
//使用前先初始化,然后加边,dfs1(rt,rt),dfs2(rt,rt),build(1,1,n),使用封装好的函数修改+查询
namespace hld{//heavy-light decomposition
int n,father[maxn],son[maxn],depth[maxn],siz[maxn];//父节点,重儿子节点,深度,子树大小
int tim=0,dfn[maxn],rk[maxn],top[maxn];//计数器,时间戳(节点编号),访问顺序,节点所在重链的顶部节点
using type=long long;//每个节点的类型
type w[maxn];//节点dfs序对应权值
void init(int N)
{
n=N;
// cerr<<"hld::n = "<<hld::n<<endl;
tim=tot=0;
memset(head,-1,sizeof(head));
memset(depth,0,sizeof(depth));
// memset(father,0,sizeof(father));
memset(son,0,sizeof(son));
// memset(top,0,sizeof(top));
memset(dfn,0,sizeof(dfn));
}
void dfs1(int x,int fa)
{//预处理出深度,父节点,重儿子,子树大小
depth[x]=depth[fa]+1;
father[x]=fa;
siz[x]=1;
int maxsiz=-1;
for(int i=head[x];~i;i=e[i].nex)
{//遍历儿子节点
int v=e[i].v;
if(v==fa)
continue;
// val[v]=e[i].w;//以点代边:将边的权值赋给边头节点
// fprintf(stderr,"val[%lld]=%lld\n",v,val[v]);
dfs1(v,x);
siz[x]+=siz[v];//加上儿子的子树大小
if(maxsiz<siz[v])
{
son[x]=v;
maxsiz=siz[v];//记录重儿子
}
}
}
void dfs2(int x,int t)
{//按dfs序对各节点重新编号,并记录对应权值到w数组
dfn[x]=++tim;//记录dfs序
rk[tim]=x;//记录访问节点的顺序,即dfn的反函数
top[x]=t;//注意这里,top是在树外的
w[tim]=val[x];//将x结点权值存到对应的时间戳
if(!son[x])//没有儿子
return;
dfs2(son[x],t);//继续处理重儿子
for(int i=head[x];~i;i=e[i].nex)//处理其他儿子
if(e[i].v!=father[x]&&e[i].v!=son[x])
dfs2(e[i].v,e[i].v);//开始另一条重链
}
int lca(int x,int y)
{
while(top[x]!=top[y])
{
if(depth[top[x]]<depth[top[y]])
swap(x,y);
x=father[top[x]];
}
return (depth[x]>depth[y])?y:x;
}
struct node//线段树按dfs序维护树上路径权值部分
{
type val,Max,lazy;
} tree[maxn<<2];
inline void pushup(int root)
{
tree[root].val=tree[root<<1].val+tree[root<<1|1].val;
tree[root].Max=max(tree[root<<1].Max,tree[root<<1|1].Max);
}
void build(int root=1,int l=1,int r=n)
{
tree[root].lazy=0;
if(l==r)//注意这里是l
tree[root].val=tree[root].Max=w[l];//按时间戳顺序的数组
else{
int mid=(l+r)>>1;
build(root<<1,l,mid);
build(root<<1|1,mid+1,r);
pushup(root);
}
}
inline void pushdown(int root,int l,int r)
{
if(tree[root].lazy)
{
int mid=(l+r)>>1;
tree[root<<1].val=tree[root<<1].val+tree[root].lazy*(mid-l+1);
tree[root<<1|1].val=tree[root<<1|1].val+tree[root].lazy*(r-mid);
tree[root<<1].Max+=tree[root].lazy;//子节点最大值也要+更新值
tree[root<<1|1].Max+=tree[root].lazy;
tree[root<<1].lazy+=tree[root].lazy;
tree[root<<1|1].lazy+=tree[root].lazy;
tree[root].lazy=0;
}
}
void modify(int root,int nst,int ned,int ust,int ued,type num)
{//区间更新
if(ned<ust||ued<nst)
return;
if(ust<=nst&&ued>=ned)
{
tree[root].val=tree[root].val+(ned-nst+1)*num;
tree[root].Max+=num;
tree[root].lazy+=num;
return;
}
pushdown(root,nst,ned);
int mid=(nst+ned)>>1;
modify(root<<1,nst,mid,ust,ued,num);
modify(root<<1|1,mid+1,ned,ust,ued,num);
pushup(root);
}
type query(int root,int nst,int ned,int qst,int qed)
{//查询区间和
if(ned<qst||qed<nst)
return 0;
if(qst<=nst&&ned<=qed)
return tree[root].val;
pushdown(root,nst,ned);
int mid=(nst+ned)>>1;
return query(root<<1,nst,mid,qst,qed)+query(root<<1|1,mid+1,ned,qst,qed);
}
type qmax(int root,int nst,int ned,int qst,int qed)
{//查询区间和
if(ned<qst||qed<nst)
return LLONG_MIN;//这里也要改成对应的MIN
if(qst<=nst&&ned<=qed)
return tree[root].Max;
pushdown(root,nst,ned);
int mid=(nst+ned)>>1;
return max(qmax(root<<1,nst,mid,qst,qed),qmax(root<<1|1,mid+1,ned,qst,qed));
}
//以下是开放的
inline void mson(int x,type addnum,int n=n)
{//将以x为根的子树全部加上一个数
modify(1,1,n,dfn[x],dfn[x]+siz[x]-1,addnum);//子树节点编号是连续的
}
inline type sonsum(int x,int n=n)
{//查询以x为根子树权值和
return query(1,1,n,dfn[x],dfn[x]+siz[x]-1);//同上
}
type sonmax(int x,int n=n)
{
return qmax(1,1,n,dfn[x],dfn[x]+siz[x]-1);
}
void mchain(int x,int y,type addnum,int n=n)
{
while(top[x]!=top[y])//不在同一条链上时
{
if(depth[top[x]]<depth[top[y]])
swap(x,y);//保证x所在链顶部更低
modify(1,1,n,dfn[top[x]],dfn[x],addnum);//更新顶部节点较低的重链(顶部节点到当前点部分)
x=father[top[x]];//跳到链顶节点的父节点
}
if(depth[x]>depth[y])//直到最后在同一条重链上
swap(x,y);//此时保证x节点在y上面
modify(1,1,n,dfn[x],dfn[y],addnum);
}
type chainsum(int x,int y,int n=n)
{
type ret=0;
while(top[x]!=top[y])
{
if(depth[top[x]]<depth[top[y]])
swap(x,y);
ret+=query(1,1,n,dfn[top[x]],dfn[x]);
x=father[top[x]];
}
if(depth[x]>depth[y])
swap(x,y);
return ret+query(1,1,n,dfn[x],dfn[y]);
}
type chainmax(int x,int y,int n=n)
{
type ret=LLONG_MIN;//注意这里改成对应的MIN
while(top[x]!=top[y])
{
if(depth[top[x]]<depth[top[y]])
swap(x,y);
ret=max(ret,qmax(1,1,n,dfn[top[x]],dfn[x]));
x=father[top[x]];
}
if(depth[x]>depth[y])
swap(x,y);
return max(ret,qmax(1,1,n,dfn[x],dfn[y]));
}
}

P3384 树链剖分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf=0x3f3f3f3f;
const int maxn=100005;
int mod=100000007;
int head[maxn],cnt=0;
struct edge
{
int v,nex;
} e[maxn<<1];
int father[maxn],son[maxn];//父节点,重儿子节点
int depth[maxn],siz[maxn],top[maxn];//深度,子树大小,节点所在重链的顶部节点
int tim=0,dfn[maxn],rk[maxn],w[maxn];//计数器,时间戳(节点编号),访问顺序
int val[maxn];//给定的每个节点的权值
void add(int u,int v)
{
e[++cnt].v=v;
e[cnt].nex=head[u];
head[u]=cnt;
}
void dfs1(int x,int fa)
{
father[x]=fa;
depth[x]=depth[fa]+1;
siz[x]=1;
int maxsize=-1;
for(int i=head[x];~i;i=e[i].nex)//遍历儿子节点
{
int v=e[i].v;
if(v==fa)
continue;
dfs1(v,x);
siz[x]+=siz[v];//加上儿子的子树大小
if(siz[v]>maxsize)
{
maxsize=siz[v];
son[x]=v;//记录重儿子
}
}
}
void dfs2(int x,int t)//当前节点与重链顶节点
{
top[x]=t;//记录该节点所在重链的顶部节点
dfn[x]=++tim;//记录该节点的访问时间(给节点编号,方便线段树操作)
rk[tim]=x;//记录访问节点的顺序
w[tim]=val[x];//将x结点权值存到对应的时间戳
if(!son[x])
return;//没有儿子
dfs2(son[x],t);//继续处理重儿子
for(int i=head[x];~i;i=e[i].nex)
{//处理其他儿子
if(e[i].v!=son[x]&&e[i].v!=father[x])
dfs2(e[i].v,e[i].v);//开始另一条重链
}
}
struct node
{
int val,lazy;
} tree[maxn<<2];
inline void pushup(int root)
{
tree[root].val=(tree[root<<1].val+tree[root<<1|1].val)%mod;
}
void build(int root,int l,int r)
{
tree[root].val=tree[root].lazy=0;
if(l==r)//注意这里是l
tree[root].val=w[l]%mod;//按时间戳顺序的数组
else{
int mid=(l+r)>>1;
build(root<<1,l,mid);
build(root<<1|1,mid+1,r);
pushup(root);
}
}
inline void pushdown(int root,int l,int r)
{
if(tree[root].lazy)
{
int mid=(l+r)>>1;
tree[root<<1].val=(tree[root<<1].val%mod+(tree[root].lazy%mod*(mid-l+1))%mod)%mod;
tree[root<<1|1].val=(tree[root<<1|1].val%mod+(tree[root].lazy%mod*(r-mid)%mod))%mod;
tree[root<<1].lazy=(tree[root<<1].lazy%mod+tree[root].lazy%mod)%mod;
tree[root<<1|1].lazy=(tree[root<<1|1].lazy%mod+tree[root].lazy%mod)%mod;
tree[root].lazy=0;
}
}
void modify(int root,int nst,int ned,int ust,int ued,int num)
{
if(ned<ust||ued<nst)
return;
if(ust<=nst&&ued>=ned)
{
tree[root].lazy=(tree[root].lazy%mod+num%mod)%mod;
tree[root].val=(tree[root].val%mod+((ned-nst+1)%mod*(num%mod))%mod)%mod;
return;
}
pushdown(root,nst,ned);
int mid=(nst+ned)>>1;
modify(root<<1,nst,mid,ust,ued,num);
modify(root<<1|1,mid+1,ned,ust,ued,num);
pushup(root);
}
int query(int root,int nst,int ned,int qst,int qed)
{
if(ned<qst||qed<nst)
return 0;
if(qst<=nst&&qed>=ned)
{
return tree[root].val%mod;
}
pushdown(root,nst,ned);
int mid=(nst+ned)>>1;
return (query(root<<1,nst,mid,qst,qed)+query(root<<1|1,mid+1,ned,qst,qed))%mod;
}
inline void mson(int x,int n,int addnum)
{//将以x为根的子树全部加上一个数
modify(1,1,n,dfn[x],dfn[x]+siz[x]-1,addnum);//子树节点编号是连续的
}
inline int qson(int x,int n)
{
return query(1,1,n,dfn[x],dfn[x]+siz[x]-1)%mod;//同上
}
void mchain(int x,int y,int n,int addnum)
{
addnum%=mod;
while(top[x]!=top[y])//不在同一条链上时
{
if(depth[top[x]]<depth[top[y]])
swap(x,y);//保证x所在链顶部更低
modify(1,1,n,dfn[top[x]],dfn[x],addnum);//更新顶部节点较低的重链(顶部节点到当前点部分)
x=father[top[x]];//跳到链顶节点的父节点
}
if(depth[x]>depth[y])//直到最后在同一条重链上
swap(x,y);//此时保证x节点在y上面
modify(1,1,n,dfn[x],dfn[y],addnum);
}
int qchain(int x,int y,int n)
{
int ret=0;
while(top[x]!=top[y])
{
if(depth[top[x]]<depth[top[y]])
swap(x,y);
ret=(ret+query(1,1,n,dfn[top[x]],dfn[x]))%mod;
x=father[top[x]];
}
if(depth[x]>depth[y])
swap(x,y);
return (ret+query(1,1,n,dfn[x],dfn[y]))%mod;
}
int main()
{
// freopen("P3384.in","r",stdin);
int n,m,p,r;
scanf("%d%d%d%d",&n,&m,&r,&mod);
memset(head,-1,sizeof(head));
for(int i=1;i<=n;i++)
scanf("%d",&val[i]);
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
add(u,v);
add(v,u);
}
dfs1(r,r);
dfs2(r,r);
build(1,1,n);
while(m--)
{
int ope,x,y,z;
scanf("%d",&ope);
if(ope==1)
{//链x->y修改,全部加上z
scanf("%d%d%d",&x,&y,&z);
mchain(x,y,n,z);
}
else if(ope==2)
{//链x->y查询
scanf("%d%d",&x,&y);
printf("%d\n",qchain(x,y,n));
}
else if(ope==3)
{//x子树修改
scanf("%d%d",&x,&z);
mson(x,n,z);
}
else{//x子树查询
scanf("%d",&x);
printf("%d\n",qson(x,n));
}
}
// for(int i=1;i<=n;i++)
// printf("%d:%d\n",i,tree[i].val);
return 0;
}
/*
*功能:
*1.更新/查询某个节点子树的权值
*2.更新/查询树上两个节点间所有点的权值
*性质:
*1.子树的时间戳一定全部小于父节点,并且连续
*2.任何一条路径都是由重链的一部分与重链间的叶子节点构成
*3.任何父节点都一定在一条重链上
*/

点分治

树的重心

  • 也叫树的质心。找到一个点,其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心,删去重心后,生成的多棵树尽可能平衡。

流程

  1. 找出当前树的重心
    • 因为分治步骤二需要将sum赋值为当前树大小(siz[v]),所以getrt要跑两遍
  2. 处理经过中心的路径
    • 点分治运算的核心,经常会出现变形
  3. 删除树的重心
  4. 对新得到的子树重复上述步骤

例题

一棵n节点的树,询问树上距离为k的点对是否存在。离线操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=10005;
const int maxk=10000005;
int tot=0,head[maxn];
struct edge
{
int v,nex,w;
} e[maxn<<1];
inline void add(int u,int v,ll w)
{
e[++tot].v=v;
e[tot].nex=head[u];
e[tot].w=w;
head[u]=tot;
}
int n,m,root,sum=0;//重心,sum当前大小
int siz[maxn],maxp[maxn];
bool vis[maxn];
void getrt(int x,int fa)
{//DFS找重心
siz[x]=1,maxp[x]=0;//maxp为最大子树大小
for(int i=head[x];~i;i=e[i].nex)
{
int v=e[i].v;
if(v==fa||vis[v])
continue;
getrt(v,x);
siz[x]+=siz[v];
if(siz[v]>maxp[x])
maxp[x]=siz[v];//记录下面的最大子树大小
}//无根树,sum-siz[x]为以x的父节点为根的大小
//在以自身为根节点的子树大小和以x的父节点为根的大小中取较大的
maxp[x]=max(maxp[x],sum-siz[x]);//sum为整棵树的大小
if(maxp[x]<maxp[root])
root=x;//最大子树最小的点为重心
}
int dist[maxn],tmp[maxn],cnt=0;//cnt计数器
void getdist(int x,int fa)
{//DFS求各点到root的距离,记录在tmp中
tmp[++cnt]=dist[x];
for(int i=head[x];~i;i=e[i].nex)
{
int v=e[i].v;
if(v==fa||vis[v])
continue;
dist[v]=dist[x]+e[i].w;
getdist(v,x);
}
}
int q[105];//q记录询问距离
bool jud[maxk],ans[105];//存放之前子树中的存在路径长度,ans判断k是否存在
queue<int> QwQ;
void solve(int x)
{//处理经过根节点x的路径
for(int i=head[x];~i;i=e[i].nex)
{
int v=e[i].v;
if(vis[v])//该点已经被去掉
continue;
cnt=0;
dist[v]=e[i].w;//设置root与儿子的距离
getdist(v,x);
for(int j=1;j<=cnt;j++)//遍历该子树上的距离
for(int k=1;k<=m;k++)//遍历询问
if(q[k]>=tmp[j])//有拼出来的可能性
ans[k]|=jud[q[k]-tmp[j]];//可以用之前以x为顶的距离拼起来
for(int j=1;j<=cnt;j++)//将这棵子树的距离存起来
{//供之后的以x为节点的子树拼路径使用
QwQ.push(tmp[j]);
jud[tmp[j]]=1;
}
}
while(!QwQ.empty())
{
jud[QwQ.front()]=0;
QwQ.pop();
}
}
void divide(int x)
{
vis[x]=jud[0]=1;//去掉根节点x
solve(x);//处理所有经过x的路径
for(int i=head[x];~i;i=e[i].nex)
{
int v=e[i].v;
if(vis[v])
continue;
maxp[root=0]=sum=siz[v];//重心置为0,maxp[0]置为最大值(所以要重新DFS计算siz)
getrt(v,0);//在以v为根的子树上找重心
getrt(root,0);//处理出以v为根的siz数组
divide(root);
}
}
int main()
{
int k,u,v;
ll w;
memset(head,-1,sizeof(head));
cin>>n>>m;
for(int i=1;i<n;i++)
{//点u到点v距离为w
cin>>u>>v>>w;
add(u,v,w);
add(v,u,w);
}
for(int i=1;i<=m;i++)
cin>>q[i];
maxp[0]=sum=n;//置为最大值
getrt(1,0);
getrt(root,0);//更新以重心为根的siz数组
divide(root);
for(int i=1;i<=m;i++)
cout<<(ans[i]?"AYE":"NAY")<<'\n';
return 0;
}

树上启发式合并(DSU on tree)

解决离线子树查询问题,即统计树上一个节点的子树中具有某种特征的节点数。

一般也可以使用DFS序莫队或DFS序主席树做。

时间复杂度$O(nlogn)$,空间复杂度$O(n)$。

流程

  1. 先用dfs处理出重儿子
  2. 使用DFS处理各子树信息,设当前子树根节点为x
    • 遍历x的轻儿子,计算轻儿子子树贡献,记录到ans数组,信息不做保留。
    • 处理x的重儿子子树贡献,记录到ans数组,并保留。
    • 暴力统计节点x及所有轻儿子子树贡献,与x的重儿子子树贡献汇总,一同回溯到上一级,以便处理出以x节点的父节点为根的子树的贡献。

例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=100005;
int head[maxn],tot;
struct Edge
{
int v,nex;
} e[maxn<<1];
inline void add(int u,int v)
{
e[++tot].v=v;
e[tot].nex=head[u];
head[u]=tot;
}
int siz[maxn],son[maxn];//记录当前节点的重儿子
void dfs(int x,int fa)//找重儿子
{
siz[x]=1;
for(int i=head[x];~i;i=e[i].nex)
{
int v=e[i].v;
if(v==fa)
continue;
dfs(v,x);
siz[x]+=siz[v];
if(siz[v]>siz[son[x]])
son[x]=v;
}
}
int color[maxn],cnt[maxn];//存放各个节点颜色,cnt存放当前树中各颜色数量
ll ans[maxn],sum;//答案数组,当前子树答案
int flag,maxc;//标记重儿子,更新最大color数量
void cal(int x,int fa,int val)//暴力更新x节点及其轻儿子子树贡献
{//val=1表示增加贡献,-1表示消去贡献
cnt[color[x]]+=val;
if(cnt[color[x]]>maxc)
{
sum=color[x];//更新数量最多的颜色之和
maxc=cnt[color[x]];
}
else if(cnt[color[x]]==maxc)
sum+=color[x];//颜色之和
for(int i=head[x];~i;i=e[i].nex)
{
int v=e[i].v;
if(v==fa||v==flag)
continue;
cal(v,x,val);
}
}
void dfs(int x,int fa,bool keep)
{
for(int i=head[x];~i;i=e[i].nex)
{
int v=e[i].v;
if(v==fa||v==son[x])
continue;
dfs(v,x,0);
}
if(son[x])//处理所有轻儿子节点后,处理重儿子子树
{
dfs(son[x],x,1);
flag=son[x];//标记当前重儿子,防止统计x及轻儿子时访问x的重儿子
}
cal(x,fa,1);//处理x节点自身颜色
ans[x]=sum;
flag=0;//置0,防止影响计算
if(!keep)//轻儿子不做保留,消去信息
{//重儿子保留,回溯到上一级时
// cal(x,fa,-1);//这么写复杂度会低些
memset(cnt,0,sizeof(cnt));
sum=maxc=0;
}
}
int main()//求每个子树上出现次数最多的数字之和
{
memset(head,-1,sizeof(head));
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>color[i];
for(int i=1;i<n;i++)
{
int u,v;
cin>>u>>v;
add(u,v);
add(v,u);
}
dfs(1,0);
dfs(1,0,0);
for(int i=1;i<=n;i++)
cout<<ans[i]<<' ';
return 0;
}

二分图

二分图如果不考虑复杂度的话,可以用网络流来做。

定义

  • 最大匹配:二分图中边集的数目最大的匹配。
  • 匹配:子图$M$上任意两条边都不依附于同一个顶点,则称$M$为原图的匹配。
  • 点覆盖:图$G=(V,E)$中的一个点覆盖为一个集合$S⊆V$使得每条边至少有一个端点在$S$中。
  • 最小点覆盖:点个数最少的$S$集合。
  • 最小边覆盖:用最少不相交简单路径覆盖DAG所有顶点。
  • 最小点权覆盖:覆盖每个节点都需要一定代价,覆盖所有边总代价最小的点集。
  • 最大独立集:在点集$V$中选出$M$个点,$M$中点与点两两无边,并使$M$最大。

常见二分图结论

  • 最小点覆盖=二分图最大匹配
  • 最小边覆盖=顶点数-最小顶点覆盖(二分图最大匹配)
  • 最大独立集=顶点数-最大匹配数
  • 所有回路长度均为偶数

适用任意图的结论

  • 对于不存在孤立点的图,最大匹配+最小边覆盖=顶点数

  • 最大独立集+最小顶点覆盖=顶点数

二分图判定(黑白染色法)

A、B集合分别染成不同颜色(由边链接的两节点颜色一定不同)

A集合中选取一个起始节点,将邻接的点染成与其不同的颜色,如果邻接的点有相同颜色的,则说明不是二分图

匈牙利算法

邻接表

时间复杂度O(nm),有重边时使用邻接矩阵?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const int maxn=205;//在主函数内向关系图G中加边
vector<int> G[maxn];//注意使用前clear()
int linker[maxn];
bool used[maxn];
bool dfs(int x)//
{
for(auto &v:G[x])
if(!used[v])//在右边找
{
used[v]=1;
if(!linker[v]||dfs(linker[v]))
{
linker[v]=x;//记录右边v号匹配
return 1;
}
}
return 0;//未找到增广路
}
int hungry(int n)
{
int ans=0;
memset(linker,0,sizeof(linker));
for(int i=1;i<=n;i++)//遍历左面
{
memset(used,0,sizeof(used));
if(dfs(i))//能找到增广路
ans++;
}
return ans;
}

KM算法

  • 二分图最佳完美匹配:带权二分图,求一种完备匹配方案,使得所有匹配边的权和最大
BFS,复杂度稳的一批

复杂度$O(n^3)$,使用时清空w数组,并且按照关系为w赋值,调用KM(n)即可得到匹配边权值和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const int maxn=1005;
int w[maxn][maxn];//二分图间的权值
int lx[maxn],ly[maxn];
int linker[maxn];//B图匹配到的A图节点
int slack[maxn];
bool visy[maxn];//记录每一轮B图匹配过
int pre[maxn];
void bfs(int k,int n)
{
int x,y=0,yy=0,delta;
memset(pre,0,sizeof(pre));
for(int i=1;i<=n;i++)
slack[i]=INF;
linker[y]=k;
while(1)
{
x=linker[y];
delta=INF;
visy[y]=true;
for(int i=1;i<=n;i++)
{
if(!visy[i])
{
if(slack[i]>lx[x]+ly[i]-w[x][i])
{
slack[i]=lx[x]+ly[i]-w[x][i];
pre[i]=y;
}
if(slack[i]<delta)
delta=slack[i],yy=i;
}
}
for(int i=0;i<=n;i++)
{
if( visy[i] )
lx[linker[i]]-=delta,ly[i]+=delta;
else
slack[i]-=delta;
}
y=yy;
if(linker[y]==-1)
break;
}
while(y)
linker[y]=linker[pre[y]],y=pre[y];
}
int KM(int n)
{
memset(lx,0,sizeof(lx));
memset(ly,0,sizeof(ly));
memset(linker,-1,sizeof(linker));
for(int i=1;i<=n;i++)
{
memset(visy,false,sizeof(visy));
bfs(i,n);
}
int ans=0;
for(int i=1;i<=n;i++)
{
if(linker[i]!=-1)
ans+=w[linker[i]][i];
}
return ans;
}
左右数目不等的模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
int wx[maxn],wy[maxn],match[maxn];
int mp[maxn][maxn],slack[maxn],pre[maxn];
bool viy[maxn];
void bfs(int k,int n,int m)
{
int py=0,px,yy=0,delta;
match[py]=k;
for(int i=0;i<=n;i++)pre[i]=0,slack[i]=inf;
do
{
px=match[py],delta=inf,viy[py]=1;
for(int i=1;i<=n;i++)
{
if(!viy[i])
{
if(wx[px]+wy[i]-mp[px][i]<slack[i])slack[i]=wx[px]+wy[i]-mp[px][i],pre[i]=py;
if(slack[i]<delta)delta=slack[i],yy=i;
}
}
for(int i=0;i<=n;i++)
{
if(viy[i])wx[match[i]]-=delta,wy[i]+=delta;
else slack[i]-=delta;
}
py=yy;
}while(match[py]!=0);
while(py)match[py]=match[pre[py]],py=pre[py];
}
int km(int n,int m)
{//n>=m,mp[m][n]这样输入匹配权值
for(int i=1;i<=n;i++)
wy[i]=0,match[i]=0;
for(int i=1;i<=m;i++)
{
wx[i]=0;
for(int j=1;j<=n;j++)
wx[i]=max(wx[i],mp[i][j]);
}
for(int i=1;i<=m;++i)
memset(viy,0,sizeof(viy)),bfs(i,n,m);
int ans=0;
for(int i=1;i<=n;++i)
ans+=wx[match[i]]+wy[i];
return ans;
}
CSL的写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
const int maxn = 305;
const int INF=0x3f3f3f3f;//KM算法:带权的二分图中寻找*权值和最大*的完备匹配
int cost[maxn][maxn];//A[i]连接B[j]的权值
int lx[maxn], ly[maxn];
int match[maxn], slack[maxn];//B[i]匹配到的A,
int previous[maxn];
bool vy[maxn];//匹配过
void augment(int root,int n)
{
fill(vy + 1, vy + n + 1, false);
fill(slack + 1, slack + n + 1, INF);
int py;
match[py = 0] = root;
do
{
vy[py] = true;
int x = match[py], yy;
int delta = INF;
for (int y = 1; y <= n; y++)
{
if (!vy[y])
{
if (lx[x] + ly[y] - cost[x][y] < slack[y])
slack[y] = lx[x] + ly[y] - cost[x][y], previous[y] = py;
if (slack[y] < delta)
delta = slack[y], yy = y;
}
}
for (int y = 0; y <= n; y++)
{
if (vy[y])
lx[match[y]] -= delta, ly[y] += delta;
else
slack[y] -= delta;
}
py = yy;
}
while(match[py] != -1);
do
{
int pre = previous[py];
match[py] = match[pre], py = pre;
} while (py);
}
int KM(int n)
{
for (int i = 1; i <= n; i++)
{
lx[i] = ly[i] = 0;
match[i] = -1;
for (int j = 1; j <= n; j++)
lx[i] = max(lx[i], cost[i][j]);
}
int answer = 0;
for (int root = 1; root <= n; root++)
augment(root,n);
for (int i = 1; i <= n; i++)
answer += lx[i], answer += ly[i];
return answer;
}

网络流

注意反向思考,添加新节点进行限流。

最小点覆盖=最大匹配 (与最大流最小割定理等价)。
最大独立集=点数-最大匹配 (独立集为点覆盖的补集)。
最小边覆盖=最大独立集 (独立集中每个点需要一条边去覆盖)。

最大独立集

方格取数问题

二分图最大独立集=顶点数-最大匹配数=点数-最小割

如果是点有点权,要求集合内两两无冲的情况下求集合最大价值。那么建立二分图,超级源点$s$和超级汇点$t$,因为图必须是二分图,分成黑点和白点两部分,黑点中与白点有关系,则连接一条容量为$inf$的边,黑点向$s$连一条容量为点价值的边,同样白点向$t$连一条容量为点价值的边。跑最大流求得最小割。

最大权闭合子图

定义有向图$G=(V,E)$的一个闭合子图是该有向图的一个点集,其中这个点集中的所有点的出边连向的还是点集中的点

解法

加入超级源点$S$和超级汇点$T$,构造网络$G=(V,E)$:

  • $S$与所有正权点连边,容量为正权点权值。
  • 负权点向$T$连边,容量为负权点的绝对值。
  • 0权点无影响可以任意当作正权点或负权点。
  • 原图中的边保持不变,容量为$INF$。

原图对应的最大权闭合图的权值和为所有正权点的权值和减去最小割

最大密度子图

定义一个无向图$G=(V,E)$的密度$D$为边数$|E|$和点数$|V|$的比值,即$D = \dfrac{|E|}{|V|}$.

最大密度子图$G’$即为无向图$G$的一个子图且具有最大的$D$.

解法1.二分,复杂度$logn.maxflow(n+m,n+m)$

二分答案$g$,下界为$\dfrac{1}{n}$,上界为$\dfrac{m}{1}$.

构造一个函数$h(g)=max(|E’|-g|V’|)$,子图.

设最优密度为$D$,当$h(g)=0$时$g$即为答案.
$$
\begin{cases}
h(g)<0 & g>D\
h(g)=0 & g=D\
h(g)>0 & g<D
\end{cases}
$$
img

对于每条边$e<u,v> \in E$,加入子图的前提是$u,v$都在子图中,所以这部分可用最大闭合子图解决:

  • 每条边$e$作为一个点,$e$向这条边的两个端点$u,v$连$INF$的边.
  • s向所有原图中的边连边,容量为$1$.
  • 原图中的点向t连边,容量为$g$.
img

搜索残余网络中源点可以到达的点即可得到子图中的点。

POJ-3155 Hard Life

抱歉这题我写疯了过不去,贴了别人的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
/*
translation:
公司里面有若干职员,每个职员都有一个或者若干个和自己不和的人,如果让两个不和的人在一起工作,则效率必然下降。
定义一个hard因子。hard:=互相不和的职工对数 / 总人数。现在求从公司里面选出一些人出来,使得hard因子最大。
solution:
网络流解决最大密度子图。
paper题,参考论文“最小割模型在信息学竞赛中的应用”一文。
*/
using namespace std;
const int maxn = 116;
const double INF = 0x3fffffff;
const double eps = 1e-8;
struct Edge
{
int to, rev;
double cap;
Edge(int to_, int rev_, double cap_):to(to_),rev(rev_),cap(cap_){}
};
vector<Edge> G[maxn];
vector<int> ans;
int level[maxn], n, m, s, t;
int iter[maxn], sum;
struct node
{
int first, second;
} p[1024];
bool vis[maxn];
int d[maxn];
void add_edge(int from, int to, double cap)
{
//printf("add edge from %d to %d, cap = %lf\n", from, to, cap);
G[from].push_back(Edge(to, G[to].size(), cap));
G[to].push_back(Edge(from, G[from].size() - 1, 0.0));
}

void bfs(int s)
{
memset(level, -1, sizeof(level));
queue<int> q;
level[s] = 0;
q.push(s);
while(!q.empty()) {
int v = q.front(); q.pop();
for(int i = 0; i < G[v].size(); i++) {
Edge& e = G[v][i];
if(e.cap > eps && level[e.to] < 0) {
level[e.to] = level[v] + 1;
q.push(e.to);
}
}
}
}
double min(double a, double b)
{
return a > b ? b : a;
}
double dfs(int v, int t, double f)
{
//printf("@%d %d\n", v, t);
if(v == t) return f;
for(int& i = iter[v]; i < G[v].size(); i++) {
Edge& e = G[v][i];
if(e.cap > 0 && level[e.to] > level[v]) {
double dt = dfs(e.to, t, min(f, e.cap));
if(dt > eps) {
e.cap -= dt;
G[e.to][e.rev].cap += dt;
return dt;
}
}
}
return 0;
}
double max_flow(int s, int t)
{
double flow = 0;
for(;;) {
bfs(s);
if(level[t] < 0) return flow;
memset(iter, 0, sizeof(iter));
double f;
while((f = dfs(s, t, INF)) > eps)
flow += f;
}
}
bool check(double k)
{
for(int i = 0; i < maxn; i++) G[i].clear();
for(int i = 1; i <= n; i++) {
add_edge(s, i, m);
add_edge(i, t, m + 2 * k - d[i]);
}
for(int i = 0; i < m; i++) {
int u = p[i].first, v = p[i].second;
add_edge(u, v, 1.0);
add_edge(v, u, 1.0);
}
double tmp = (n * m - max_flow(s, t)) / 2;
if(tmp > eps) return true;
else return false;
}
void dfs1(int v)
{
vis[v] = true;
for(int i = 0; i < G[v].size(); i++) {
Edge e = G[v][i];
if(e.cap > eps && !vis[e.to])
dfs1(e.to);
}
}
int main()
{
//freopen("in.txt", "r", stdin);
while(~scanf("%d%d", &n, &m)) {
memset(d, 0, sizeof(d));
for(int i = 0; i < m; i++) {
scanf("%d%d", &p[i].first, &p[i].second);
d[p[i].first]++;
d[p[i].second]++;
}
if(m == 0) {
printf("1\n1\n");
} else {
s = 0; t = n + 1;
double lb = 0.0, ub = m * 1.0, mid;
double precision = (double)(1.0 / n / n);
while(ub - lb >= precision) {
mid = (lb + ub) / 2;
//printf("# %lf %lf\n", lb, ub);
if(check(mid)) lb = mid;
else ub = mid;
}
check(lb);
memset(vis, 0, sizeof(vis));
dfs1(0);
sum = 0;
for(int i = 1; i <= n; i++)
if(vis[i]) sum++;
printf("%d\n", sum);
for(int i = 1; i <= n; i++)
if(vis[i]) printf("%d\n", i);
}
}
return 0;
}

以及下面这个,我和他写的一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include<iostream>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=40018,inf=1e9;
struct edg{
int nxt,to;
double f;
}e[maxn];
int cnt,vis[maxn],head,tail,q[maxn],S,T,n,m,t,last[maxn],d[maxn];
double l,r,mid;
void add(int x,int y,double z){
++t;e[t].nxt=last[x];last[x]=t;e[t].to=y;e[t].f=z;
++t;e[t].nxt=last[y];last[y]=t;e[t].to=x;e[t].f=0;
}
struct dui{
int from,to;
}qq[maxn];
int bfs(){
head=tail=0;
memset(d,-1,sizeof(d));
q[++tail]=S;d[S]=0;
while(head<tail){
int u=q[++head];
for(int i=last[u];i;i=e[i].nxt){
int v=e[i].to;
if(e[i].f&&d[v]==-1){
d[v]=d[u]+1;
q[++tail]=v;
}
}
}
return d[T]!=-1;
}
double dfs(int x,double h){
if(x==T){return h;}
double tmp=0,cp;
for(int i=last[x];i;i=e[i].nxt){
int v=e[i].to;
if(e[i].f&&d[v]==d[x]+1){
cp=dfs(v,min(h-tmp,e[i].f));
e[i].f-=cp;e[i^1].f+=cp;tmp+=cp;
if(tmp==h){return tmp;}
}
}
if(!tmp)d[x]=-2;
return tmp;
}
int pd(double mid){
memset(last,0,sizeof(last));t=1;
for(int i=1;i<=m;++i){
add(n+i,qq[i].from,inf);
add(n+i,qq[i].to,inf);
add(S,n+i,1.0);
}
for(int i=1;i<=n;++i){
add(i,T,mid);
}
double pp,res=0;
while(bfs()){
while(pp=dfs(S,inf))res+=pp;
}
//cout<<res<<' '<<m<<endl;
return (double(m)-res)>0;
}
void dfs1(int x){
vis[x]=1;
if(x>=1&&x<=n)cnt++;
for(int i=last[x];i;i=e[i].nxt){
int v=e[i].to;
if(e[i].f&&!vis[v])dfs1(v);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;++i){
scanf("%d%d",&qq[i].from,&qq[i].to);
}
if(!m){puts("1");puts("1");return 0;}
l=0,r=m+1;S=n+m+1,T=n+m+2;
while(r-l>=1.0/n/n){
//cout<<l<<' '<<r<<endl;
mid=(l+r)/2;
if(pd(mid))l=mid;
else r=mid;
}
pd(l);
dfs1(S);
printf("%d\n",cnt);
for(int i=1;i<=n;++i){
if(vis[i])
printf("%d\n",i);
}
//system("pause");
return 0;
}

模板部分

dinic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
struct Dinic
{//复杂度O(n^2m)
struct Edge
{
int from, to, cap, flow;
Edge(int u, int v, int c, int f):
from(u), to(v), cap(c), flow(f) {}
};
int n, m, s, t; //结点数,边数(包括反向弧),源点编号和汇点编号
vector<Edge> edges; //边表。edge[e]和edge[e^1]互为反向弧
vector<int> G[maxn]; //邻接表,G[i][j]表示节点i的第j条边在e数组中的序号
bool vis[maxn]; //BFS使用
int d[maxn]; //从起点到i的距离
int cur[maxn]; //当前弧下标
void init(int n)
{
this->n = n;
for (int i = 0; i < n; i++) G[i].clear();
edges.clear();
}
void AddEdge(int from, int to, int cap)
{
edges.push_back(Edge(from, to, cap, 0));
edges.push_back(Edge(to, from, 0, 0));//反向弧,初始容量为0
m = edges.size();
G[from].push_back(m - 2);
G[to].push_back(m - 1);
}
bool BFS()
{
memset(vis, 0, sizeof(vis));
// memset(d, 0, sizeof(d));
queue<int> q;
q.push(s);
d[s] = 0;
vis[s] = 1;
while (!q.empty())
{
int x = q.front();
q.pop();
for (int i = 0; i < G[x].size(); i++)
{
Edge& e = edges[G[x][i]];
if (!vis[e.to] && e.cap > e.flow)
{//只考虑残量网络中的弧
vis[e.to] = 1;
d[e.to] = d[x] + 1;//构造分层图
q.push(e.to);
}
}
}
return vis[t];//有无增广路,s->t
}
int DFS(int x, int a)//x为当前点,a为当前边上流量
{//在层次图上向t延伸,多路增广
if(x==t||a==0)//到达目标/流量为0断流
return a;
int flow = 0, f;
for(int& i=cur[x];i<G[x].size();i++)//从上一次x遍历跑到的点开始跑
{//从上次考虑的弧
Edge& e = edges[G[x][i]];
if(d[x]+1==d[e.to]&&(f=DFS(e.to,min(a,e.cap-e.flow))) > 0)
{//只从层数编号较小的点到下一层的点
e.flow += f;//该路径上边流量都增加f
edges[G[x][i]^1].flow -= f;//方便反悔
flow += f;
a -= f;//用去可增广量
if(a==0)//a等于0及时退出
break;//当a!=0,说明当前节点还存在另一个增广路分支。
}
}
if(!flow)//增广后容量满了
d[x] = -1;//炸点优化,不必要的点下一次就不用遍历
return flow;//返回x节点最大流量,传递到上一级
}
int Maxflow(int s, int t)
{
this->s = s, this->t = t;
int flow = 0;
while (BFS())//不停地用bfs构造分层网络,然后用dfs沿着阻塞流增广
{
memset(cur, 0, sizeof(cur));//建完分层图后cur也要初始化
flow += DFS(s,inf);
}
return flow;
}
} di;
dijkstra费用流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
struct MCMF {
using type = ll;//有时费用是double
struct Edge {
ll v, cap;
type cost;
ll rev;
};
const type INF=0x3f3f3f3f3f3f3f3f;
ll flow, s, t, n;//跑完的最大流
type cost;//跑完最大流下的最小费用
type dist[maxn], H[maxn];//H为节点势函数
ll pv[maxn], pe[maxn];//前驱结点和对应边
std::vector<Edge> G[maxn];//因为要记录前驱,不能用前向星
void init(int n){
this->n = n;
for (int i = 0; i <= n; ++i) G[i].clear();
}
void addEdge(int u, int v, int cap, type cost){//dijk费用流中两节点间流向单向
G[u].push_back({v,cap,cost,G[v].size()});
G[v].push_back({u,0,-cost,G[u].size()-1});
}
bool dijkstra()//这里是单路增广
{//复杂度相对SPFA稳定
std::priority_queue<pair<type,ll>, std::vector<pair<type,ll>>, std::greater<pair<type,ll>> > q;
std::fill(dist, dist + n + 1, INF);
dist[s] = 0; q.push({ 0, s });
while (!q.empty())
{//到x距离即为到x的单位花费之和
pair<type,ll> x = q.top(); q.pop();
ll& u = x.second;
if (dist[u] < x.first) continue;//不能优化距离
for (int i = 0; i < G[u].size(); ++i)
{
Edge& e = G[u][i];//当前边
ll& v = e.v;
pair<type,ll> y(dist[u] + e.cost + H[u] - H[v], v);
if (e.cap > 0 && dist[v] > y.first)
{
dist[v] = y.first;
pv[v] = u,pe[v] = i;//前驱点与前驱边
q.push(y);
}
}
}
if (dist[t] == INF)//无法增广
return false;
for (int i = 0; i <= n; ++i)//更新每轮的势函数
H[i] += dist[i];
ll f = INF;
for (int v = t; v != s; v = pv[v])//沿增广路回到起点
f = std::min(f, G[pv[v]][pe[v]].cap);
flow += f;//每次增广一条路径,这条路径增广量就是新增的流量
cost += f * H[t];//h[t]-h[s]即为s到t的路径长
for (int v = t; v != s; v = pv[v])
{//更新增广路容量信息
Edge& e = G[pv[v]][pe[v]];
e.cap -= f;
G[v][e.rev].cap += f;
}
return true;
}
ll solve(int s, int t)
{
this->s = s, this->t = t;
flow = cost = 0;
std::fill(H, H + n + 1, 0);//初始网络为0非负,势函数也为0
while (dijkstra());//每次选择最小费用增广路径一定是当前残留图的最小增广路径
return flow;
}
} mm;
BF费用流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
struct MCMF
{
struct Edge
{
int from, to, cap, flow, cost;
Edge(int u, int v, int c, int f, int w)
: from(u), to(v), cap(c), flow(f), cost(w) {}
};
int n, m;
vector<Edge> edges;
vector<int> G[maxn];
int inq[maxn]; //是否在队列中
int d[maxn]; //bellmanford
int p[maxn]; //上一条弧
int a[maxn]; //可改进量
void init(int n)
{
this->n = n;
for (int i = 0; i <= n; i++)
G[i].clear();
edges.clear();
}
void AddEdge(int from, int to, int cap, int cost)
{
// edges.emplace_back(Edge(from, to, cap, 0, cost));
// edges.emplace_back(Edge(to, from, 0, 0, -cost));
edges.push_back(Edge(from, to, cap, 0, cost));
edges.push_back(Edge(to, from, 0, 0, -cost));
m = edges.size();
G[from].push_back(m - 2);
G[to].push_back(m - 1);
}
bool BellmanFord(int s, int t, int& flow, ll& cost)
{
for (int i = 0; i < n; i++)
d[i] = inf;
memset(inq, 0, sizeof(inq));
d[s] = 0;
inq[s] = 1;
p[s] = 0;
a[s] = inf;
queue<int> q;
q.push(s);
while (!q.empty())
{
int u = q.front();
q.pop();
inq[u] = 0;
for (int i = 0; i < G[u].size(); i++)
{
Edge& e = edges[G[u][i]];
if (e.cap > e.flow && d[e.to] > d[u] + e.cost)
{
d[e.to] = d[u] + e.cost;
p[e.to] = G[u][i];
a[e.to] = min(a[u], e.cap - e.flow);
if (!inq[e.to])
{
q.push(e.to);
inq[e.to] = 1;
}
}
}
}
// cout<<"DEBUG:BellmanFord\n";
// cout<<"flow"<<flow<<",cost="<<cost<<endl;
if (d[t] == inf)
return false; // 当没有可增广的路时退出
flow += a[t];
cost += (ll)d[t] * (ll)a[t];
for (int u = t; u != s; u = edges[p[u]].from)
{
edges[p[u]].flow += a[t];
edges[p[u] ^ 1].flow -= a[t];
}
return true;
}
int MincostMaxflow(int s, int t, ll& cost)
{
int flow = 0;
cost = 0;
while(BellmanFord(s, t, flow, cost));
return flow;
}
} mm;

对偶图最小割

最大流

太空飞行计划问题(最小割,收益最大问题)

最优收益 = 所有实验的报酬总和 - 该图的最大流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int m,n;//实验数,仪器数
scanf("%d%d",&m,&n);
int s=0,t=m+n+1,sum=0;
di.init(t);
for(int i=1;i<=m;i++)
{
int profit,equ;
scanf("%d",&profit);
sum+=profit;//sum为所有收益之和
di.AddEdge(s,i,profit);//源点到实验
while(1)
{
char ch;
scanf("%d%c",&equ,&ch);
di.AddEdge(i,m+equ,inf);//实验到器材
if(ch=='\r'||ch=='\n')
break;
}
}
for(int i=1;i<=n;i++)
{
int cost;
scanf("%d",&cost);
di.AddEdge(m+i,t,cost);//器材到汇点
}
int ans=di.Maxflow(s,t);
for(int i=1;i<=m;i++)
{
if(di.d[i])//选择去做的实验编号
printf("%d ",i);
}
putchar('\n');
for(int i=1;i<=n;i++)
{
if(di.d[m+i])//需要购买的器材编号
printf("%d ",i);
}
printf("\n%d\n",sum-ans);

建图后跑最大流求出最小割,满流的实验边割掉,满流的设备边也在割集里(但是这是需要购买的),该最小割即为最小亏损。
所以最后一次BFS,所有未被割掉的实验为非饱和弧,可以求出深度。
所以未被割掉的实验(及选择去做的实验)所连接的设备同样可以求出深度。

最小路径覆盖问题(最大流,最小不相交路径覆盖模型,要求路径数最少,路径输出)

二分图定理之一:最小路径覆盖数=顶点数-最大匹配。

覆盖所有的边:每条边下界设为1, 然后求最小流。 覆盖所有的点:建立二分图,对于$u\rightarrow v$的边,看做二分图中的$<u,v>$,然后答案为点数-最大匹配。

不要使用炸点优化!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//#pragma G++ optimize("O2")
const int maxn = 13005;//上大的模板
const int inf=0x3f3f3f3f;
int nex[maxn];
bool vist[maxn];
int all;
struct Dinic
{
struct Edge
{
int from, to, cap, flow;
Edge(int u, int v, int c, int f):
from(u), to(v), cap(c), flow(f) {}
};
int n, m, s, t; //结点数,边数(包括反向弧),源点编号和汇点编号
vector<Edge> edges; //边表。edge[e]和edge[e^1]互为反向弧
vector<int> G[maxn]; //邻接表,G[i][j]表示节点i的第j条边在e数组中的序号
bool vis[maxn]; //BFS使用
int d[maxn]; //从起点到i的距离
int cur[maxn]; //当前弧下标
void init(int n)
{
this->n = n;
for (int i = 0; i <= n; i++)
G[i].clear();
edges.clear();
}
inline void AddEdge(int from, int to, int cap)
{
edges.emplace_back(Edge(from, to, cap, 0));//魔改蔡队模板
edges.emplace_back(Edge(to, from, 0, 0)); //反向弧
this->m = edges.size();
G[from].push_back(m - 2);
G[to].push_back(m - 1);
}
bool BFS()
{
memset(vis, 0, sizeof(vis));
memset(d, 0, sizeof(d));
queue<int> q;
q.push(s);
d[s] = 0;
vis[s] = 1;
while (!q.empty())
{
int x = q.front();
q.pop();
for (int i = 0; i < G[x].size(); i++)
{
Edge& e = edges[G[x][i]];
if (!vis[e.to] && e.cap > e.flow)
{
vis[e.to] = 1;
d[e.to] = d[x] + 1;
q.push(e.to);
}
}
}
return vis[t];
}
int DFS(int x, int a)//x为当前点,a为当前边上流量
{
if (x == t || a == 0)//到达目标/流量为0
return a;
int flow = 0, f;
for (int& i = cur[x]; i < G[x].size(); i++)
{ //从上次考虑的弧
Edge& e = edges[G[x][i]];
if (d[x] + 1 == d[e.to] && (f = DFS(e.to, min(a, e.cap - e.flow))) > 0)
{
if(x!=s)//为了输出路径,添加此语句
{
nex[x]=e.to;//记录下一个节点,便于输出
vist[e.to-all]=1;//打标记,找起点
}
e.flow += f;
edges[G[x][i] ^ 1].flow -= f;
flow += f;
a -= f;
if (a == 0)
break;
}
}//这题不能乱用炸点优化!!!
return flow;
}
int Maxflow(int s, int t)
{
this->s = s, this->t = t;
int flow = 0;
while (BFS())
{
memset(cur, 0, sizeof(cur));
flow += DFS(s,inf);
}
return flow;
}
} di;
int main()
{//给定DAG图G
int n,m;//n为DAG顶点数,m为边数
memset(vist,0,sizeof(vist));
cin>>n>>m;
all=n;
int s=0,t=2*n+1;
di.init(t);
for(int i=1;i<=n;i++)//拆点,分别连接s和t
{
di.AddEdge(s,i,1);
di.AddEdge(n+i,t,1);
}
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
di.AddEdge(u,n+v,1);//容量为1
}
int ans=di.Maxflow(s,t);
for(int i=1;i<=n;i++)
{
if(!vist[i])//没标记的就是起点
{
int now=i;
cout<<now<<' ';
while(nex[now]&&nex[now]!=t)
{
cout<<nex[now]-n<<' ';
now=nex[now]-n;
}
cout<<endl;
}
}
cout<<n-ans<<endl;//最少路径数
return 0;
}

P2774 方格取数问题(最大流,最小割)

m*n 个方格棋盘中,每个方格中有一个正整数。从方格中取数,使任意 2 个数所在方格没有公共边,且取出的数的总和最大。

思路:黑白染色,按奇偶性建立二分图,奇连s,偶连t。枚举每个奇数性质的方格,连接相邻的偶数性质的方格。跑dinic求出最小割,总值减去最小割即为答案。

带权最大独立集=所有点的点权-最小割。因为是最小割的剩余图,所以剩下的点之间不会有联系,即为符合要求的答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
int main()
{
int m,n;
scanf("%d%d",&m,&n);//棋盘的行数和列数
int s=0,t=m*n+1,sum=0;//sum为总价值
di.init(t+1);
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
int val,no;
scanf("%d",&val);//i行j列的数值
sum+=val;
no=n*(i-1)+j;//重点:一行n个
if((i+j)%2){//相邻的i+j奇偶性必定相斥
di.AddEdge(s,no,val);//根据奇偶性连接
}
else{
di.AddEdge(no,t,val);
}
}
}//s与t的边插入结束
int nx[4]={0,-1,0,1};//只枚举两个方向是不够的,因为是有向图
int ny[4]={-1,0,1,0};
for(int i=1;i<=m;i++)//m行
for(int j=1;j<=n;j++)//n列
{
int no=n*(i-1)+j;
if((i+j)%2){//二分图左边的节点,开始连接右面的节点
for(int k=0;k<4;k++)//枚举方向,相邻的四个方格
{
int x=i+nx[k],y=j+ny[k];//x与m轴同方向
if(x<=0||x>m||y<=0||y>n)
continue;
int no2=n*(x-1)+y;//相邻的方格编号
di.AddEdge(no,no2,inf);
}
}
}
printf("%d\n",sum-di.Maxflow(s,t));
return 0;
}

费用流

P4012 深海机器人问题(物品在网格上)

物品在网格的边上,每条网格的边有权值,交叉点作为节点,在图上添加点和点之间的边,设容量为1,花费为-cost。

注意设置容量inf,费用为0的经过边,跑最小费用最大流,答案取负。

P3356 火星探险问题(物品在交叉点上)

物品在节点上,而且有障碍。

思路:拆点,若不为障碍,拆点间添加容量为inf,价值为0的边;若该点有所需物品,两点间额外添加一条容量为1价值为-val的边;

跑最小费用最大流即可,使用DFS输出路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
for(int i=1;i<=n;i++)
{
flag=0;
mm.dfs(1,1,1,i);//起点(1,1),出点1,第i号机器人
}
void MCMF::dfs(int x,int y,int u,int no)//第no号机器人到达了u节点
{
int kx,ky,dir;//0向下,1向右
int res=p*q+u;//u节点的拆点编号
if(flag)//no号到达了终点
return;
for(int i=0;i<G[res].size();i++)
{
if(flag)
return;
if(edges[G[res][i]].flow>0&&(edges[G[res][i]].to==u+1||edges[G[res][i]].to==u+p))//残余流量大于0
{//q行,p列,x与q同方向,表示第几行
edges[G[res][i]].flow--;
if(edges[G[res][i]].to==u+1)//向右
dir=1;
else
dir=0;
printf("%d %d\n",no,dir);
if(dir==1)
dfs(x,y+1,u+1,no);
// printf("(%d,%d)->(%d,%d)\n",x,y,x,y+1);
else
dfs(x+1,y,u+p,no);
// printf("(%d,%d)->(%d,%d)\n",x,y,x+1,y);
if(edges[G[res][i]].to==p*q)
flag=1;
}
}
}
P2604 扩容费用问题

问题:给出DAG每条边的容量和扩容费用,1.求出1到n的最大流。2.求出1到n的最大流增加K所需的最小扩容费用。

先跑一个零费用最大流。利用残余网络进行费用流扩容,将原来的0费用网络添加新的费用边,并进行限流,cost即为扩容费用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int n,m,k;
cin>>n>>m>>k;
mm.init(n+10);
for(int i=1;i<=m;i++){
cin>>e[i].u>>e[i].v>>e[i].c>>e[i].w;
mm.AddEdge(e[i].u,e[i].v,e[i].c,0);//费用为0,跑最大流
}
ll cost;
cout<<mm.MincostMaxflow(1,n,cost)<<' ';
for(int i=1;i<=m;i++)//给原来每条边上添加新边,
mm.AddEdge(e[i].u,e[i].v,inf,e[i].w);
int s=0;//超级源点s
mm.AddEdge(s,1,k,0);//s到1进行限流
mm.MincostMaxflow(s,n,cost);//费用流
cout<<cost<<endl;//扩容费用