图论
这里用类似邻接表的方法存图。有的算法可能需要邻接矩阵,详见模板·线性代数。
struct Graph
{
struct Vertex
{
vector<int> a,b;//相关出边和入边编号
int siz,dep,top,dfn;//树链剖分中使用,依次代表子树节点数、深度、所在链的顶端节点、dfs序
};
struct Edge
{
int from,to;
ll dist,cap;//边长、容量,图论算法使用
};
vector<Vertex> v;//点集
vector<Edge> e;//边集
Graph(int n):v(n) {}
void add(const Edge &ed)
{
if(ed.from==ed.to)return;//如果有需要请拆点
v[ed.from].a.push_back(e.size());
v[ed.to].b.push_back(e.size());
e.push_back(ed);
}
};
树
struct Tree:Graph
{
Tree(int n):Graph(n) {}
int fa(int k,int i=0)
{
return e[v[k].b[i]].from;
}
int ch(int k,int i=0)
{
return e[v[k].a[i]].to;
}
void build(int u,const Graph &g)//无向图dfs建树,且重边在最前,u为根节点
{
v[u].siz=1;
for(int i=0,w,k; i!=g.v[u].a.size(); ++i)
if(k=g.v[u].a[i],w=g.e[k].to,!v[w].siz)//没访问过的点siz默认0
{
build(w,g);
v[u].siz+=v[w].siz;
add(g.e[k]);
if(v[ch(u)].siz<v[w].siz)//重边移到最前
swap(v[u].a.front(),v[u].a.back());
}
}
};
树链剖分与LCA
struct Diagram:Tree
{
Fenwick data;//暂用树状数组作为默认数据结构
Diagram(const Graph &g,int root):
Tree(g.v.size()),data(g.v.size())
{
build(root,g);
int cnt=v[root].dfn=v[root].dep=1;
dfs(v[root].top=root,cnt);
}
void dfs(int u,int &cnt)
{
for(int i=0,w; i!=v[u].a.size(); ++i)
{
v[w=ch(u,i)].dfn=++cnt;
v[w].top=i?w:v[u].top;
v[w].dep=v[u].dep+1;
dfs(w,cnt);
}
}
int lca(int x,int y)
{
for(; v[x].top!=v[y].top; x=fa(v[x].top))
if(v[v[x].top].dep<v[v[y].top].dep)swap(x,y);
if(v[x].dep<v[y].dep)swap(x,y);
return y;
}
ll ask(int x,int y)
{
ll ans=0;
for(; v[x].top!=v[y].top; x=fa(v[x].top))
{
if(v[v[x].top].dep<v[v[y].top].dep)swap(x,y);
ans+=data.ask(v[v[x].top].dfn,v[x].dfn);
}
if(v[x].dep<v[y].dep)swap(x,y);
return ans+=data.ask(v[y].dfn,v[x].dfn);
}
void add(int x,int y,ll pv)
{
for(; v[x].top!=v[y].top; x=fa(v[x].top))
{
if(v[v[x].top].dep<v[v[y].top].dep)swap(x,y);
data.add(v[v[x].top].dfn,v[x].dfn,pv);
}
if(v[x].dep<v[y].dep)swap(x,y);
data.add(v[y].dfn,v[x].dfn,pv);
}
};
点剖(点分治)
零号点为虚节点。
struct TreeDiv:Graph
{
int root;
vector<int> vis,siz,mx;
TreeDiv(int n):Graph(n),vis(n),siz(n),mx(n,n) {}
void dfsRoot(int u,int fa)
{
for(int i=mx[u]=siz[u]=0,k,to; i<v[u].a.size(); ++i)
if(k=v[u].a[i],to=e[k].to,to!=fa&&!vis[to])
if(dfsRoot(to,u),siz[u]+=siz[to],mx[u]<siz[to])
mx[u]=siz[to];
if(mx[u]<mx[0]-++siz[u])mx[u]=mx[0]-siz[u];
if(mx[root]>mx[u])root=u;
}
void dfsDist(int u,int fa,ll d)
{
//用d更新答案
for(int i=0,k,to; i<v[u].a.size(); ++i)
if(k=v[u].a[i],to=e[k].to,to!=fa&&!vis[to])
dfsDist(to,u,d+e[k].dist);
}
int cal(int u,ll d)//返回符合要求的点对数
{
return dfsDist(u,0,d),/*得到答案*/;
}
void dfs(int u=1)
{
dfsRoot(u,root=0),ans+=cal(u=root,0),vis[u]=1;
for(int i=0,k,to; i<v[u].a.size(); ++i)
if(k=v[u].a[i],to=e[k].to,!vis[to])
ans-=cal(to,e[k].dist),mx[0]=siz[to],dfs(to);
}
};
最小生成树
无向图
同时给出Prim算法(生成新树)、Kruskal算法(消耗小)。
struct Prim:Tree
{
struct DistGreater
{
bool operator()(const Edge &e1,const Edge &e2)
{
return e1.dist>e2.dist;
}
};
ll ans;
vector<int> vis;
priority_queue<Edge,vector<Edge>,DistGreater> q;
Prim(const Graph &g,int root):Tree(n),ans(0),vis(g.v.size(),0)//生成新树,每条边都要有等长反向边
{
for(insert(root,g); !q.empty();)
{
Edge ed=q.top();
if(q.pop(),!vis[ed.to])
{
insert(ed.to,g);
ans+=ed.dist;
add(ed);
}
}
}
void insert(int u,const Graph &g)//把点和对应的相连的边加入集合
{
vis[u]=1;
for(int i=0,k; i!=g.v[u].a.size(); ++i)
if(k=g.v[u].a[i],!vis[g.e[k].to])
q.push(g.e[k]);
}
};
ll kruskal(vector<Edge> &e,int n)//会清空边集e,每条边被认作无向边
{
ll ret=0;
UnionFindSet ufs(n);
for(sort(e.begin(),e.end(),DistGreater()); !e.empty(); e.pop_back())
if(ufs.fa(e.back().from)!=ufs.fa(e.back().to))
{
ufs.merge(e.back().from,e.back().to);
ret+=e.back().dist;
}
return /*ufs.siz>1?INF:*/ret;//视情况选择去注释
}
有向图
指定以root为根,如果没有限定根那么新建一个虚拟点作为根,向所有边连边长最大边长+1的边,在最后生成的图中去掉此边。时间复杂度 O ( V E ) O(VE) O(VE)。
ll zhuLiu(vector<Edge> &e,int root,int n)//不存在返回INF
{
for(ll ret=0;;)
{
vector<ll> in(n,INF);
vector<int> pre(n,NPOS);
for(int i=0,to; i<e.size(); ++i)
{
if(e[i].from==(to=e[i].to))
swap(e[i--],e.back()),e.pop_back();
else if(in[to]>e[i].dist)
in[to]=e[i].dist,pre[to]=e[i].from;
}
for(int i=in[root]=0; i<n; ++i)
if(in[i]==INF)return INF;
vector<int> id(n,NPOS),vis(n,NPOS);
int tn=0;
for(int i=0,v; i<n; ++i)
{
for(ret+=in[v=i]; vis[v]!=i&&id[v]==NPOS&&v!=root; v=pre[v])
vis[v]=i;
if(v!=root&&id[v]==NPOS)
{
for(int u=pre[v]; u!=v; u=pre[u])
id[u]=tn;
id[v]=tn++;
}
}
if(!tn)return ret;
for(int i=0; i<n; ++i)
if(id[i]==NPOS)id[i]=tn++;
for(int i=0,v; i<e.size(); ++i)
if((e[i].from=id[e[i].from])!=(e[i].to=id[v=e[i].to]))
e[i].dist-=in[v];
n=tn,root=id[root];
}
}
连通性
无向图求割和双连通分量
割边:在连通图中,删除了连通图的某条边后,图不再连通。这样的边被称为割边,也叫做桥。
割点:在连通图中,删除了连通图的某个点以及与这个点相连的边后,图不再连通。这样的点被称为割点。
构造dfs搜索树,在树上有两类节点可以成为割点:
对根节点u,若其有两棵或两棵以上的子树,则该根结点u为割点;
对非根非叶节点u,若其中的某棵子树的节点均没有指向u的祖先节点的回边,说明删除u之后,根结点与该棵子树的节点不再连通;则节点u为割点。
对于一个无向图的子图,当删除其中任意一条边后,不改变图内点的连通性,这样的子图叫做边的双连通子图。而当子图的边数达到最大时,叫做边的双连通分量。原理是图中所有割边再求一次SCC,可直接使用下面求SCC的代码。
对于一个无向图的子图,当删除其中任意一个点后,不改变图内点的连通性,这样的子图叫做点的双连通子图。而当子图的边数达到最大时,叫做点的双连通分量。下面给出求点双连通分量的代码。
struct BCC:Graph//Biconnected Connected Componenet
{
vector<int> low,bid,stak,cutPoint,cutEdge;//连通块最早dfs序,边的端点所属双连通块
int bcc_siz;
BCC(int n):Graph(n) {}
void ask()
{
low.assign(v.size(),NPOS);
bid.assign(e.size(),NPOS);
cutPoint.assign(v.size(),0);
cutEdge.assign(e.size(),0);
for(int i=bcc_siz=0,cnt=0; i<v.size(); ++i)
if(low[i]==NPOS)
dfs(i,NPOS,cnt);
}
void dfs(int u,int fa,int &cnt)
{
low[u]=v[u].dfn=++cnt;
for(int i=0,k,to,ch=0; i<v[u].a.size(); ++i)
if(k=v[u].a[i],to=e[k].to,to!=fa)
{
if(low[to]==NPOS)
{
++ch;
stak.push_back(k);
dfs(to,u,cnt);
low[u]=min(low[u],low[to]);
if(low[to]>=v[u].dfn)
for(++bcc_siz,cutPoint[u]=fa!=NPOS||ch>1;;)
{
int x=stak.back();
stak.pop_back();
bid[x]=bid[x^1]=bcc_siz-1;
if(x==k)break;
}
if(low[to]>v[u].dfn)cutEdge[k]=cutEdge[k^1]=1;
}
else if(v[to].dfn<v[u].dfn)
{
stak.push_back(k);
low[u]=min(low[u],v[to].dfn);
}
}
}
};
双连通图的构造
先求出所有的桥,然后删除这些桥边,剩下的每个连通块都是一个双连通子图。把每个双连通子图收缩为一个顶点,再把桥边加回来,最后的这个图一定是一棵树,边连通度为1。统计出树中度为1的节点的个数,即为叶节点的个数,记为leaf
。至少在树上添加(leaf+1)/2
条边,就能使树达到边双连通:先把两个最近公共祖先最远的两个叶节点之间连接一条边,这样可以把这两个点到祖先的路径上所有点收缩到一起,因为一个形成的环一定是双连通的;然后再找两个最近公共祖先最远的两个叶节点,这样一对一对找完,恰好是(leaf+1)/2
次,把所有点收缩到了一起。
有向图求强连通分量
如果是无向图,求出来的还是边双连通分量。
struct SCC:Graph//Strongly Connected Componenet
{
vector<int> low,sid,stak;//连通块最早dfs序,点所属连通块
int scc_siz;
SCC(int n):Graph(n) {}
void ask()
{
low.assign(v.size(),NPOS);
sid.assign(v.size(),NPOS);
for(int i=scc_siz=0,cnt=0; i!=v.size(); ++i)
if(low[i]==NPOS)
dfs(i,NPOS,cnt);
}
void dfs(int u,int fa,int &cnt)
{
low[u]=v[u].dfn=++cnt;
stak.push_back(u);
for(int i=0,k,to; i!=v[u].a.size(); ++i)
if(k=v[u].a[i],to=e[k].to,to!=fa,1)//求边双连通分量把",1"注释掉,即不许走回边
{
if(low[to]==NPOS)
dfs(to,u,cnt),low[u]=min(low[u],low[to]);
else if(sid[to]==NPOS)
low[u]=min(low[u],v[to].dfn);
}
if(low[u]==v[u].dfn)
for(++scc_siz;;)
{
int x=stak.back();
stak.pop_back();
sid[x]=scc_siz-1;
if(x==u)break;
}
}
};
2-SAT
n个布尔变量 x 0 … x n − 1 x_0\ldots x_{n-1} x0…xn−1,逻辑表达式 Y = ( A 0 + B 0 ) ( A 1 + B 1 ) … ( A m − 1 + B m − 1 ) Y=(A_0+B_0)(A_1+B_1)\ldots(A_{m-1}+B_{m-1}) Y=(A0+B0)(A1+