图论

作者 Overstars

https://blog.csdn.net/Miaplacidus

常见概念

  • 简单图:不含有平行边和环的图。
  • 多重图:含有平行边(同向)的图。
  • 完全图:每一对节点之间都有边的简单图,$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$。
  • git config –global core.autocrlf false和git config –global core.safecrlf true还有git init

弦图

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

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

#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})$。

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快。

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实现(求最短路)
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优化(负环检测)

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

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;//未找到负权环
}

Floyd

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];
}
}

最大团

最大团大小

#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];
bool dfs(int dep,int num)//深度(团大小),备选集合
{
if(num==0){
if(dep>ans){
ans=dep;
return 1;
}
return 0;
}
for(int i=1;i<=num;i++){
if(dep+num-i+1<=ans)
return 0;
int v=tmp[dep][i];
if(dep+f[v]<=ans)
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;
}
if(dfs(dep+1,cnt))
return 1;
}
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;
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_j$向$x_i$连一条权值为$c_k$的边。

最小生成树(MST)

Prim算法

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

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,重复上述步骤。

代码
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的树根。
  • 异或生成树:字典树合并连通块

次小生成树

严格次小生成树(洛谷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)$

#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;
}

虚树

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

步骤

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

代码示例

sort(h+1,h+k+1,[](const int &x,const int &y){
return dfn[x]<dfn[y];//按dfs序排序
});
stk[top=1]=h[1];
cnt2=0;
for(int i=2;i<=k;i++)
{
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入栈
adde(lc,stk[top]);
stk[top]=lc;
}//否则为情况1
stk[++top]=now;
}
while(--top)
adde(stk[top],stk[top+1]);

拓扑排序

只适用于DAG。

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

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()$未找到,则说明该边为补边。

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;
}

强连通分量

Tarjan

Tarjan缩点+DAG 拓扑排序dp

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

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

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;
}
Tarjan无向图求割点
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;
}
无向图求桥
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\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$ $a\rightarrow b,\lnot b\rightarrow\lnot a$
$\lnot a\rightarrow\lnot b$ $\lnot a\rightarrow\lnot b,b\rightarrow a$

最近公共祖先(LCA)

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

倍增

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

    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(logn \times logn)$修改,$O(logn \times logn)$查询。

修改差分数组之前先用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$

查询

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 树链剖分

通用模板

int n,tot=0,head[maxn];
ll val[maxn];//给定的每个节点的权值
struct edge
{//边权一般不必记录到这里
int v,w,nex;
} e[maxn<<1];
inline void add(int u,int v,int 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 father[maxn],son[maxn],depth[maxn],siz[maxn];//父节点,重儿子节点,深度,子树大小
int tim=0,dfn[maxn],rk[maxn],top[maxn];//计数器,时间戳(节点编号),访问顺序,节点所在重链的顶部节点
ll w[maxn];//节点dfs序对应权值
void init()
{
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;//以点代边:将边的权值赋给边头节点
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序维护树上路径权值部分
{
ll 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,int l,int r)
{
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,ll 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);
}
ll 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);
}
ll qmax(int root,int nst,int ned,int qst,int qed)
{//查询区间和
if(ned<qst||qed<nst)
return LLONG_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,int n,ll addnum)
{//将以x为根的子树全部加上一个数
modify(1,1,n,dfn[x],dfn[x]+siz[x]-1,addnum);//子树节点编号是连续的
}
inline ll sonsum(int x,int n)
{//查询以x为根子树权值和
return query(1,1,n,dfn[x],dfn[x]+siz[x]-1);//同上
}
ll sonmax(int x,int n)
{
return qmax(1,1,n,dfn[x],dfn[x]+siz[x]-1);
}
void mchain(int x,int y,int n,ll addnum)
{
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);
}
ll chainsum(int x,int y,int n)
{
ll 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]);
}
ll chainmax(int x,int y,int n)
{
ll ret=LLONG_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 树链剖分

#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的点对是否存在。离线操作。

#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节点的父节点为根的子树的贡献。

二分图

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

定义

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

常见二分图结论

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

适用任意图的结论

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

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

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

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

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

匈牙利算法

邻接表

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

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)即可得到匹配边权值和。

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;
}
左右数目不等的模板
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的写法
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;
}

网络流

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

模板部分

dinic
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费用流

struct MCMF {
struct Edge {
ll v, cap, cost, rev;
};
const ll INF=0x3f3f3f3f3f3f3f3f;
ll flow, cost, s, t, n;//前驱结点和对应边
ll dist[maxn], H[maxn], pv[maxn], pe[maxn];//H为节点势函数
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, ll cost){//dojk费用流中两节点间流向单向
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<ll,ll>, std::vector<pair<ll,ll>>, std::greater<pair<ll,ll>> > q;
std::fill(dist, dist + n + 1, INF);
dist[s] = 0; q.push({ 0, s });
while (!q.empty())
{//到x距离即为到x的单位花费之和
pair<ll,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<ll,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;
}
} mcmf;

最大流

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

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

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,所有未被割掉的实验为非饱和弧,可以求出深度。
所以未被割掉的实验(及选择去做的实验)所连接的设备同样可以求出深度。

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

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

不要使用炸点优化!!!

//#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求出最小割,总值减去最小割即为答案。

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输出路径。

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即为扩容费用。

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;//扩容费用