图论 / Graph(ACM-Template 2.0)

图论

图论基础

前向星

struct edge{int to,w,nxt;}; // 指向,权值,下一条边
vector<edge> a;
int head[N];
void addedge(int x,int y,int w){
	a.push_back({y,w,head[x]});
	head[x]=a.size()-1;
}
void init(int n){
	a.clear();
	fill(head,head+n,-1);
}
// for(int i=head[x];i!=-1;i=a[i].nxt) // 遍历x出发的边(x,a[i].to)

拓扑排序 / Toposort

  • \(O(V+E)\)
vector<int> topo;
void toposort(int n){
	static int deg[N]; fill(deg,deg+n,0);
	static queue<int> q;
	repeat(x,0,n)for(auto p:a[x])deg[p]++;
	repeat(i,0,n)if(deg[i]==0)q.push(i);
	while(!q.empty()){
		int x=q.front(); q.pop(); topo.push_back(x);
		for(auto p:a[x])if(--deg[p]==0)q.push(p);
	}
}

线段树优化建图

  • 建两棵线段树,第一棵每个结点连向其左右儿子,第二棵每个结点连向其父亲,两棵树所有叶子对应连无向边。
  • add(x1, y1, x2, y2, w) 表示 \([x_1,y_1]\) 每个结点向 \([x_2,y_2]\) 每个结点连 w 边。
  • a[i+tr.n] 表示结点 i。
  • 建议 10 倍内存,编号从 0 开始,\(O(n\log n)\)
typedef vector<pii> node;
node a[N]; int top;
struct seg{
	int n;
	void init(int inn){
		for(n=1;n<inn;n<<=1); top=n*4;
		repeat(i,0,n*4)a[i].clear();
		repeat(i,1,n){
			a[i]<<pii(i*2,0);
			a[i]<<pii(i*2+1,0);
			a[i*2+n*2]<<pii(i+n*2,0);
			a[i*2+1+n*2]<<pii(i+n*2,0);
		}
		repeat(i,0,inn){
			a[i+n]<<pii(i+n*3,0);
			a[i+n*3]<<pii(i+n,0);
		}
	}
	void b_add(int l,int r,int x,int w){
		for(l+=n-1,r+=n+1;l^r^1;l>>=1,r>>=1){
			if(~l & 1)a[(l^1)+n*2]<<pii(x,w);
			if(r & 1)a[(r^1)+n*2]<<pii(x,w);
		}
	}
	void a_add(int l,int r,int x,int w){
		for(l+=n-1,r+=n+1;l^r^1;l>>=1,r>>=1){
			if(~l & 1)a[x]<<pii(l^1,w);
			if(r & 1)a[x]<<pii(r^1,w);
		}
	}
}tr;
void add(int x1,int y1,int x2,int y2,int w){
	int s=top++; a[s].clear();
	tr.b_add(x1,y1,s,w);
	tr.a_add(x2,y2,s,0);
}
int f(int x){return x+tr.n;}

最短路径

单源正权 using Dijkstra

  • 仅限正权,\(O(E\log E)\)
struct node {
	int to; ll dis;
	bool operator<(const node &b) const {
		return dis > b.dis;
	}
};
bool vis[N];
vector<node> a[N];
ll dis[N]; // result
void dij(int s, int n){ // s: start
	fill(vis, vis + n + 1, 0);
	fill(dis, dis + n + 1, INF); dis[s] = 0; // last[s] = -1;
	static priority_queue<node> q; q.push({s, 0});
	while (!q.empty()) {
		int x = q.top().to; q.pop();
		if (vis[x]) { continue; } vis[x] = 1;
		for (auto i : a[x]) {
			int p = i.to;
			if (dis[p] > dis[x] + i.dis) {
				dis[p] = dis[x] + i.dis;
				q.push({p, dis[p]});
				// last[p] = x; // last 可以记录最短路(倒着)
			}
		}
	}
}

多源 using Floyd

  • \(O(V^3)\)
repeat(k,0,n)
repeat(i,0,n)
repeat(j,0,n)
	f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
  • 补充:bitset 优化(只考虑是否可达),\(O(V^3)\)
// bitset<N> g<N>;
repeat(i,0,n)
repeat(j,0,n)
if(g[j][i])
	g[j]|=g[i];

一般单源 using SPFA

  • SPFA搜索中,有一个点入队 \(n+1\) 次即存在负环
  • 编号从 0 开始,\(O(VE)\)
int cnt[N]; bool vis[N]; ll h[N]; // h意思和dis差不多,但是Johnson里需要区分
int n;
struct node{int to; ll dis;};
vector<node> a[N];
bool spfa(int s){ // 返回是否有负环(s为起点)
	repeat(i,0,n+1)
		cnt[i]=vis[i]=0,h[i]=inf;
	h[s]=0; // last[s]=-1;
	static deque<int> q; q.assign(1,s);
	while(!q.empty()){
		int x=q.front(); q.pop_front();
		vis[x]=0;
		for(auto i:a[x]){
			int p=i.to;
			if(h[p]>h[x]+i.dis){
				h[p]=h[x]+i.dis;
				// last[p]=x; // last可以记录最短路(倒着)
				if(vis[p])continue;
				vis[p]=1;
				q.push_back(p); // 可以SLF优化
				if(++cnt[p]>n)return 1;
			}
		}
	}
	return 0;
}
bool negcycle(){ // 返回是否有负环
	a[n].clear();
	repeat(i,0,n)
		a[n].push_back({i,0}); // 加超级源点
	return spfa(n);
}

多源 using Johnson

  • SPFA + Dijkstra 实现多源最短路,编号从 0 开始,\(O(VE\log E)\)
ll dis[N][N];
bool jn(){ // 返回是否成功
	if(negcycle())return 0;
	repeat(x,0,n)
	for(auto &i:a[x])
		i.dis+=h[x]-h[i.to];
	repeat(x,0,n)dij(x,dis[x]);
	repeat(x,0,n)
	repeat(p,0,n)
	if(dis[x][p]!=inf)
		dis[x][p]+=h[p]-h[x];
	return 1;
}

最小生成树 / MST

Kruskal

  • 对边长排序,然后添边,并查集判联通,\(O(E\log E)\),排序是瓶颈
DSU d;
struct edge{int u,v,dis;}e[200010];
ll kru(){
	ll ans=0,cnt=0; d.init(n);
	sort(e,e+m);
	repeat(i,0,m){
		int x=d[e[i].u],y=d[e[i].v];
		if(x==y)continue;
		d.join(x,y);
		ans+=e[i].dis;
		cnt++;
		if(cnt==n-1)break;
	}
	if(cnt!=n-1)return -1;
	else return ans;
}

Boruvka

  • 类似Prim算法,但是可以多路增广(名词迷惑行为),\(O(E\log V)\)
DSU d;
struct edge{int u,v,dis;}e[200010];
ll bor(){
	ll ans=0;
	d.init(n);
	e[m].dis=inf;
	vector<int> b; // 记录每个联通块的增广路(名词迷惑行为)
	bool f=1;
	while(f){
		b.assign(n,m);
		repeat(i,0,m){
			int x=d[e[i].u],y=d[e[i].v];
			if(x==y)continue;
			if(e[i].dis<e[b[x]].dis)
				b[x]=i;
			if(e[i].dis<e[b[y]].dis)
				b[y]=i;
		}
		f=0;
		for(auto i:b)
		if(i!=m){
			int x=d[e[i].u],y=d[e[i].v];
			if(x==y)continue;
			ans+=e[i].dis;
			d.join(x,y);
			f=1;
		}
	}
	return ans;
}

树论

树的直径

  • 直径:即最长路径
  • 求直径:以任意一点出发所能达到的最远结点为一个端点,以这个端点出发所能达到的最远结点为另一个端点(也可以树上dp)

树的重心

  • 重心:以重心为根,其最大儿子子树最小
  • 性质
    • 以重心为根,所有子树大小不超过整棵树的一半
    • 重心最多有两个
    • 重心到所有结点距离之和最小
    • 两棵树通过一条边相连,则新树的重心在是原来两棵树重心的路径上
    • 一棵树添加或删除一个叶子,重心最多移动一条边的距离
    • 重心不一定在直径上
void dfs(int x,int fa=-1){
	static int sz[N],maxx[N];
	sz[x]=1; maxx[x]=0;
	for(auto p:a[x])if(p!=fa){
		dfs(p,x);
		maxx[x]=max(maxx[x],sz[p]);
		sz[x]+=sz[p];
	}
	maxx[x]=max(maxx[x],n-sz[x]);
	if(maxx[x]<maxx[rt])rt=x;
}

最近公共祖先 / LCA

树上倍增解法

  • 编号从哪开始都可以,初始化 \(O(n\log n)\),查询 \(O(\log n)\)
vector<int> e[N]; int dep[N],fa[N][22];
#define log(x) (31-__builtin_clz(x))
void dfs(int x){
	repeat(i,1,log(dep[x])+1){
		fa[x][i]=fa[fa[x][i-1]][i-1];
		// dis[x][i]=U(dis[x][i-1],dis[fa[x][i-1]][i-1]);
	}
	for(auto p:e[x])
	if(fa[x][0]!=p){
		fa[p][0]=x,dep[p]=dep[x]+1;
		// dis[p][0]=f(x,p);
		dfs(p);
	}
}
int lca(int x,int y){
	if(dep[x]<dep[y])swap(x,y);
	while(dep[x]>dep[y])
		x=fa[x][log(dep[x]-dep[y])];
	if(x==y)return x;
	repeat_back(i,0,log(dep[x])+1)
	if(fa[x][i]!=fa[y][i])
		x=fa[x][i],y=fa[y][i];
	return fa[x][0];
}
void init(int s){fa[s][0]=s; dep[s]=0; dfs(s);}
/*
lf len2(int x,int y){ // y是x的祖先
	lf ans=0;
	while(dep[x]>dep[y]){
		ans=U(ans,dis[x][log(dep[x]-dep[y])]);
		x=fa[x][log(dep[x]-dep[y])];
	}
	return ans;
}
lf length(int x,int y){int l=lca(x,y); return U(len2(x,l),len2(y,l));} // 无修查询链上信息
*/

欧拉序列建st表解法

  • 编号从 0 开始,初始化 \(O(n\log n)\),查询 \(O(1)\)
int n,m;
vector<int> a;
vector<int> e[500010];
bool vis[500010];
int pos[500010],dep[500010];
#define mininarr(a,x,y) (a[x]<a[y]?x:y)
struct RMQ{
	#define logN 21
	int f[N*2][logN],log[N*2];
	RMQ(){
		log[1]=0;
		repeat(i,2,N*2)
			log[i]=log[i/2]+1;
	}
	void build(){
		int n=a.size();
		repeat(i,0,n)
			f[i][0]=a[i];
		repeat(k,1,logN)
		repeat(i,0,n-(1<<k)+1)
			f[i][k]=mininarr(dep,f[i][k-1],f[i+(1<<(k-1))][k-1]);
	}
	int query(int l,int r){
		if(l>r)swap(l,r);// !!
		int s=log[r-l+1];
		return mininarr(dep,f[l][s],f[r-(1<<s)+1][s]);
	}
}rmq;
void dfs(int x,int d){
	if(vis[x])return;
	vis[x]=1;
	dep[x]=d;
	a.push_back(x);
	pos[x]=a.size()-1;
	repeat(i,0,e[x].size()){
		int p=e[x][i];
		if(vis[p])continue;
		dfs(p,d+1);
		a.push_back(x);
	}
}
int lca(int x,int y){
	return rmq.query(pos[x],pos[y]);
}
// 初始化:dfs(s,1); rmq.build();

树链剖分解法

  • 编号从哪开始都可以,初始化 \(O(n)\),查询 \(O(\log n)\)
vector<int> e[N];
int dep[N],son[N],sz[N],top[N],fa[N]; // son: heaviest son, top: top of the chain
void dfs1(int x){ // get (dep,sz,son,fa), private
	sz[x]=1;
	son[x]=-1;
	dep[x]=dep[fa[x]]+1;
	for(auto p:e[x]){
		if(p==fa[x])continue;
		fa[p]=x; dfs1(p);
		sz[x]+=sz[p];
		if(son[x]==-1 || sz[son[x]]<sz[p])
			son[x]=p;
	}
}
void dfs2(int x,int tv){ // get top, private
	top[x]=tv;
	if(son[x]==-1)return;
	dfs2(son[x],tv);
	for(auto p:e[x]){
		if(p==fa[x] || p==son[x])continue;
		dfs2(p,p);
	}
}
void init(int s){ // s is the root
	fa[s]=s;
	dfs1(s);
	dfs2(s,s);
}
int lca(int x,int y){
	while(top[x]!=top[y])
		if(dep[top[x]]>=dep[top[y]])x=fa[top[x]];
		else y=fa[top[y]];
	return dep[x]<dep[y]?x:y;
}

Tarjan解法

  • 离线算法,基于并查集
  • qry 和 ans 编号从 0 开始,\(O(n+m)\),大常数(不看好)
vector<int> e[N]; vector<pii> qry,q[N]; // qry输入
DSU d; bool vis[N]; int ans[N]; // ans输出
void dfs(int x){
	vis[x]=1;
	for(auto i:q[x])if(vis[i.fi])ans[i.se]=d[i.fi];
	for(auto p:e[x])if(!vis[p])dfs(p),d[p]=x;
}
void solve(int n,int s){
	repeat(i,0,qry.size()){
		q[qry[i].fi].push_back({qry[i].se,i});
		q[qry[i].se].push_back({qry[i].fi,i});
	}
	d.init(n); dfs(s);
}

一些关于lca的问题

int length(int x,int y){ // 路径长度
	return dep[x]+dep[y]-2*dep[lca(x,y)];
}
int intersection(int x,int y,int xx,int yy){ // 树上两条路径公共点个数
	int t[4]={lca(x,xx),lca(x,yy),lca(y,xx),lca(y,yy)};
	sort(t,t+4,[](int x,int y){return dep[x]<dep[y];});
	int r=lca(x,y),rr=lca(xx,yy);
	if(dep[t[0]]<min(dep[r],dep[rr]) || dep[t[2]]<max(dep[r],dep[rr]))
		return 0;
	int tt=lca(t[2],t[3]);
	return 1+dep[t[2]]+dep[t[3]]-dep[tt]*2;
}

树链剖分

  • 编号从 0 开始,处理链 \(O(\log^2 n)\),处理子树 \(O(\log n)\)
vector<int> e[N];
int dep[N],son[N],sz[N],top[N],fa[N];
int id[N],arcid[N],idcnt; // id[x]:结点x在树剖序中的位置,arcid相反
void dfs1(int x){
	sz[x]=1; son[x]=-1; dep[x]=dep[fa[x]]+1;
	for(auto p:e[x]){
		if(p==fa[x])continue;
		fa[p]=x; dfs1(p);
		sz[x]+=sz[p];
		if(son[x]==-1 || sz[son[x]]<sz[p])
			son[x]=p;
	}
}
void dfs2(int x,int tv){
	arcid[idcnt]=x; id[x]=idcnt++; top[x]=tv;
	if(son[x]==-1)return;
	dfs2(son[x],tv);
	for(auto p:e[x]){
		if(p==fa[x] || p==son[x])continue;
		dfs2(p,p);
	}
}
int lab[N]; // 初始点权
seg tr[N*2],*pl; // if(l==r){a=lab[arcid[l]];return;}
void init(int s){
	idcnt=0; fa[s]=s;
	dfs1(s); dfs2(s,s);
	seginit(0,idcnt-1); // 线段树的初始化
}
void upchain(int x,int y,int d){
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]])swap(x,y);
		tr->update(id[top[x]],id[x],d);
		x=fa[top[x]];
	}
	if(dep[x]>dep[y])swap(x,y);
	tr->update(id[x],id[y],d);
}
ll qchain(int x,int y){
	ll ans=0;
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]])swap(x,y);
		ans+=tr->query(id[top[x]],id[x]);
		x=fa[top[x]];
	}
	if(dep[x]>dep[y])swap(x,y);
	ans+=tr->query(id[x],id[y]);
	return ans;
}
void uptree(int x,int d){
	tr->update(id[x],id[x]+sz[x]-1,d);
}
ll qtree(int x){
	return tr->query(id[x],id[x]+sz[x]-1);
}

树分治

点分治

  • 每次找树的重心(最大子树最小的点),去掉它后对所有子树进行相同操作
  • 一般 \(O(n\log n)\)
  • 例:luogu P3806,带边权的树,询问长度为 \(q_i\) 的路径是否存在
vector<pii> a[N];
bool vis[N];
vector<pii> q; // q[i].fi: query; q[i].se: answer
namespace center{
vector<int> rec;
int sz[N],maxx[N];
void dfs(int x,int fa=-1){
	rec<<x;
	sz[x]=1; maxx[x]=0;
	for(auto i:a[x]){
		int p=i.fi;
		if(p!=fa && !vis[p]){
			dfs(p,x);
			sz[x]+=sz[p];
			maxx[x]=max(maxx[x],sz[p]);
		}
	}
}
int get(int x){ // get center
	rec.clear(); dfs(x); int n=sz[x],ans=x;
	for(auto x:rec){
		maxx[x]=max(maxx[x],n-sz[x]);
		if(maxx[x]<maxx[ans])ans=x;
	}
	return ans;
}
}
vector<int> rec;
void getdist(int x,int dis,int fa=-1){
	if(dis<10000010)rec<<dis;
	for(auto i:a[x]){
		int p=i.fi;
		if(p!=fa && !vis[p]){
			getdist(p,dis+i.se,x);
		}
	}
}
unordered_set<int> bkt;
void dfs(int x){
	x=center::get(x);
	bkt.clear(); bkt.insert(0);
	vis[x]=1;
	for(auto i:a[x]){ // 这部分统计各个子树的信息并更新答案
		int p=i.fi;
		if(!vis[p]){
			rec.clear(); getdist(p,i.se);
			for(auto i:rec){
				for(auto &j:q)
				if(bkt.count(j.fi-i))
					j.se=1;
			}
			for(auto i:rec)bkt.insert(i);
		}
	}
	for(auto i:a[x]){ // 这部分进一步分治
		int p=i.fi;
		if(!vis[p]){
			dfs(p);
		}
	}
}

虚树

  • \(O(k\log n)\) 处理出指定 k 个点及其两两 lca 构成的树,原理是单调栈
  • pos[x] 表示 DFS 序中 x 的位置,lab[x] 表示 x 是否为指定点
  • tr 表示虚树,v 是指定点序列(input)
  • 基于 lca,预处理为 lca::init() 以及 pos[]
vector<int> e[N],v; // v: input
int pos[N];
vector<int> stk,rec;
vector<pii> tr[N];
bool lab[N];
#define r stk.rbegin()
void add(){
	tr[r[1]].push_back({r[0],dep[r[0]]-dep[r[1]]}); // tr[x][i].second is the length of the edge
	rec.push_back(r[0]);
	stk.pop_back();
}
void lastdfs(int x,int fa){
	;
}
void vtree(){
	sort(v.begin(),v.end(),[](int x,int y){
		return pos[x]<pos[y];
	});
	stk.assign(1,1); rec.assign(1,1);
	for(auto i:v)lab[i]=1;
	for(auto i:v)if(i!=1){
		int l=lca(i,r[0]);
		while(pos[l]<pos[r[0]]){
			if(pos[l]>pos[r[1]])
				stk.insert(stk.end()-1,l);
			add();
		}
		stk.push_back(i);
	}
	while(stk.size()>1)add();
	// flag=true;
	lastdfs(1,-1);
	// if(flag)printf("%d\n",cost[1]); else puts("-1");
	for(auto i:rec){ // clear
		tr[i].clear();
		lab[i]=0; // cost[i]=0; up[i]=0;
	}
}

树上启发式合并

  • 暴力方式处理子树问题
  • 编号无限制,\(O(n\log n)\)
vector<int> e[N]; int n;
int sz[N],son[N],dep[N]; bool vis[N];
ll ans[N],sum[N]; int num[N],top,c[N]; // not fixed
void initdfs(int x,int fa){
	dep[x]=dep[fa]+1; sz[x]=1;
	for(auto p:e[x])if(p!=fa){
		initdfs(p,x); sz[x]+=sz[p];
		if(sz[p]>sz[son[x]])son[x]=p;
	}
}
void update(int x,int fa,int op){
	sum[num[c[x]]]-=c[x]; num[c[x]]+=op; sum[num[c[x]]]+=c[x];
	if(sum[top+1])top++; if(!sum[top])top--;
	for(auto p:e[x])if(p!=fa && !vis[p])
		update(p,x,op);
}
void dfs(int x,int fa,int hs){
	for(auto p:e[x])if(p!=fa && p!=son[x])
		dfs(p,x,0);
	if(son[x])dfs(son[x],x,1),vis[son[x]]=1;
	update(x,fa,1);
	vis[son[x]]=0; ans[x]=sum[top];
	if(!hs)update(x,fa,-1);
}
// initdfs(s,-1); dfs(s,-1,1);

联通性相关

强联通分量 SCC+缩点

Tarjan

  • 编号从0开始,\(O(V+E)\)
vector<int> a[N];
stack<int> stk;
bool vis[N],instk[N];
int dfn[N],low[N],co[N],w[N]; // co:染色结果,w:点权
vector<int> sz; // sz:第i个颜色的点数
int n,m,dcnt;
void dfs(int x){ // Tarjan求强联通分量
	vis[x]=instk[x]=1; stk.push(x);
	dfn[x]=low[x]=++dcnt;
	for(auto p:a[x]){
		if(!vis[p])dfs(p);
		if(instk[p])low[x]=min(low[x],low[p]);
	}
	if(low[x]==dfn[x]){
		int t; sz.push_back(0); // 记录
		do{
			t=stk.top();
			stk.pop();
			instk[t]=0;
			sz.back()+=w[t]; // 记录
			co[t]=sz.size()-1; // 染色
		}while(t!=x);
	}
}
void getscc(){
	fill(vis,vis+n,0);
	sz.clear();
	repeat(i,0,n)if(!vis[i])dfs(i);
}
void shrink(){ // 缩点,在a里重构
	static set<pii> eset;
	eset.clear();
	getscc();
	repeat(i,0,n)
	for(auto p:a[i])
	if(co[i]!=co[p])
		eset.insert({co[i],co[p]});
	n=sz.size();
	repeat(i,0,n){
		a[i].clear();
		w[i]=sz[i];
	}
	for(auto i:eset){
		a[i.fi].push_back(i.se);
		// a[i.se].push_back(i.fi);
	}
}
  • 例题:给一个有向图,连最少的边使其变为scc。解:scc缩点后输出 \(\max(\sum\limits_i[indeg[i]=0],\sum\limits_i[outdeg[i]=0])\),特判只有一个scc的情况

Kosaraju(缩点同上)

  • 编号从 1 开始,\(O(V+E)\)
int co[N],sz[N]; // (output) co: vertex color, sz: number of vertices of color i
bool vis[N]; vector<int> q; // private
vector<int> a[N],b[N]; // (input) a: graph, b:invgraph
int cnt; // (output) cnt: color number
void dfs1(int x){
	vis[x]=1;
	for(auto p:a[x])if(!vis[p])dfs1(p);
	q.push_back(x);
}
void dfs2(int x,int c){
	vis[x]=0; co[x]=c; sz[c]++;
	for(auto p:b[x])if(vis[p])dfs2(p,c);
}
void getscc(int n){
	fill(vis,vis+n+1,0);
	fill(sz,sz+n+1,0);
	cnt=0; q.clear();
	repeat(i,1,n+1)if(!vis[i])dfs1(i);
	reverse(q.begin(),q.end());
	for(auto i:q)if(vis[i])dfs2(i,++cnt);
}

边双连通分量 using Tarjan

  • 编号从0开始,\(O(V+E)\)
void dfs(int x,int fa){ // Tarjan求边双联通分量
	vis[x]=instk[x]=1; stk.push(x);
	dfn[x]=low[x]=++dcnt;
	for(auto p:a[x])
	if(p!=fa){
		if(!vis[p])dfs(p,x);
		if(instk[p])low[x]=min(low[x],low[p]);
	}
	else fa=-1; // 处理重边
	if(low[x]==dfn[x]){
		int t; sz.push_back(0); // 记录
		do{
			t=stk.top();
			stk.pop();
			instk[t]=0;
			sz.back()+=w[t]; // 记录
			co[t]=sz.size()-1; // 染色
		}while(t!=x);
	}
}
void getscc(){
	fill(vis,vis+n,0);
	sz.clear();
	repeat(i,0,n)if(!vis[i])dfs(i,-1);
}
// 全局变量,shrink()同scc

割点 / 割顶

  • Tarjan
bool vis[N],cut[N]; // cut即结果,cut[i]表示i是否为割点
int dfn[N],low[N];
int dcnt; // 时间戳
void dfs(int x,bool isroot=1){
	if(vis[x])return; vis[x]=1;
	dfn[x]=low[x]=++dcnt;
	int ch=0; cut[x]=0;
	for(auto p:a[x]){
		if(!vis[p]){
			dfs(p,0);
			low[x]=min(low[x],low[p]);
			if(!isroot && low[p]>=dfn[x])
				cut[x]=1;
			ch++;
		}
		low[x]=min(low[x],dfn[p]);
	}
	if(isroot && ch>=2) // 根结点判断方法
		cut[x]=1;
}

2-Sat问题

可行解

  • \(2n\) 个顶点,其中顶点 \(2i\) 和顶点 \(2i+1\) 中能且仅能选一个,边 (u, v) 表示选了 u 就必须选 v,求一个可行解
  • 暴力版,可以跑出字典序最小的解,编号从 0 开始,\(O(VE)\),(但是难以跑到上界
struct twosat{ // 暴力版
	int n;
	vector<int> g[N*2];
	bool mark[N*2]; // mark即结果,表示是否选择了这个点
	int s[N],c;
	bool dfs(int x){ // private
		if(mark[x^1])return 0;
		if(mark[x])return 1;
		mark[s[c++]=x]=1;
		for(auto p:g[x])
		if(!dfs(p))
			return 0;
		return 1;
	}
	void init(int _n){
		n=_n;
		for(int i=0;i<n*2;i++){
			g[i].clear();
			mark[i]=0;
		}
	}
	void add(int x,int y){ // 这个函数随题意变化
		g[x*2].push_back(y*2+1); // 选了x*2就必须选y*2+1
		g[y*2].push_back(x*2+1); // 选了y*2就必须选x*2+1
	}
	bool solve(){ // 返回是否存在解
		for(int i=0;i<n*2;i+=2)
		if(!mark[i] && !mark[i^1]){
			c=0;
			if(!dfs(i)){
				while(c>0)mark[s[--c]]=0;
				if(!dfs(i^1))return 0;
			}
		}
		return 1;
	}
}ts;
  • SCC操作,编号从 1 开始,\(O(V+E)\),注意 N 需要手动两倍
bool ans[N]; // shows one solution if possible
bool twosat(int n){ // return whether possible
	getscc(n*2);
	repeat(i,1,n+1)
		if(co[i]==co[i+n])return 0;
	repeat(i,1,n+1)
		ans[i]=(co[i]<co[i+n]);
	return 1;
}
  • 2-SAT计数
  • 空缺(太恐怖了)

支配树 using Lengauer-Tarjan 算法

  • 有向图给定源点,若删掉 r,源点不可达 u,则称 r 是 u 的支配点
  • 支配树即所有非源点的点与最近支配点(idom)连边形成的树(源点为根)
  • input: a 邻接表,b 反图邻接表。
  • 大约 \(O(V+E)\)
vector<int> a[N],b[N],tr[N]; // tr: result
int fa[N],dfn[N],dcnt,arcdfn[N];
int c[N],best[N],sm[N],im[N]; // im: result
void init(int n){
	dcnt=0;
	iota(c,c+n+1,0);
	repeat(i,1,n+1){
		tr[i].clear();
		a[i].clear();
		b[i].clear();
	}
	repeat(i,1,n+1)sm[i]=best[i]=i;
	fill(dfn,dfn+n+1,0);
}
void dfs(int u){
	dfn[u]=++dcnt; arcdfn[dcnt]=u;
	for(auto v:a[u])if(!dfn[v]){fa[v]=u; dfs(v);}
}
int find(int x){
	if(c[x]==x)return x;
	int &f=c[x],rt=find(f);
	if(dfn[sm[best[x]]]>dfn[sm[best[f]]])
		best[x]=best[f];
	return f=rt;
}
void solve(int s){
	dfs(s);
	repeat_back(i,2,dcnt+1){
		int x=arcdfn[i],mn=dcnt+1;
		for(auto u:b[x]){
			if(!dfn[u])continue;
			find(u); mn=min(mn,dfn[sm[best[u]]]);
		}
		c[x]=fa[x];
		tr[sm[x]=arcdfn[mn]].push_back(x);
		x=arcdfn[i-1];
		for(auto u:tr[x]){
			find(u);
			if(sm[best[u]]!=x)im[u]=best[u];
			else im[u]=x;
		}
		tr[x].clear();
	}
	repeat(i,2,dcnt+1){
		int u=arcdfn[i];
		if(im[u]!=sm[u])im[u]=im[im[u]];
		tr[im[u]].push_back(u);
	}
}

图上的NP问题

最大团 and 极大团计数

  • 求最大团顶点数(和最大团),g[][] 编号从 0 开始,\(O(\exp)\)
int g[N][N],f[N][N],v[N],Max[N],n,ans; // g[][]是邻接矩阵,n是顶点数
// vector<int> rec,maxrec; // maxrec是最大团
bool dfs(int x,int cur){
	if(cur==0)
		return x>ans;
	repeat(i,0,cur){
		int u=f[x][i],k=0;
		if(Max[u]+x<=ans)return 0;
		repeat(j,i+1,cur)
		if(g[u][f[x][j]])
			f[x+1][k++]=f[x][j];
		// rec.push_back(u);
		if(dfs(x+1,k))return 1;
		// rec.pop_back();
	}
	return 0;
}
void solve(){
	ans=0; // maxrec.clear();
	repeat_back(i,0,n){
		int k=0;
		repeat(j,i+1,n)
		if(g[i][j])
			f[1][k++]=j;
		// rec.clear(); rec.push_back(i);
		if(dfs(1,k)){
			ans++;
			// maxrec=rec;
		}
		Max[i]=ans;
	}
}
  • 求极大团个数(和所有极大团),g[][] 的编号从 1 开始!\(O(\exp)\)
int g[N][N],n;
// vector<int> rec; // 存当前极大团
int ans,some[N][N],none[N][N]; // some是未搜索的点,none是废除的点
void dfs(int d,int sn,int nn){
	if(sn==0 && nn==0)
		ans++; // 此时rec是其中一个极大图
	// if(ans>1000)return; // 题目要求_(:зゝ∠)_
	int u=some[d][0];
	for(int i=0;i<sn;++i){
		int v=some[d][i];
		if(g[u][v])continue;
		int tsn=0,tnn=0;
		for(int j=0;j<sn;++j)
		if(g[v][some[d][j]])
			some[d+1][tsn++]=some[d][j];
		for(int j=0;j<nn;++j)
		if(g[v][none[d][j]])
			none[d+1][tnn++]=none[d][j];
		// rec.push_back(v);
		dfs(d+1,tsn,tnn);
		// rec.pop_back();
		some[d][i]=0;
		none[d][nn++]=v;
	}
}
void solve(){ // 运行后ans即极大团数
	ans=0;
	for(int i=0;i<n;++i)
		some[0][i]=i+1;
	dfs(0,n,0);
}

最小染色数

  • \(O(\exp)\)n=17 可用
int n,m;
int g[N]; // 二进制邻接矩阵
bool ind[1<<N]; // 是否为(极大)独立集
int dis[1<<N];
vector<int> a; // 存独立集
#define np (1<<n)
int bfs(){ // 重复覆盖简略版
	fill(dis,dis+np,inf); dis[0]=0;
	auto q=queue<int>(); q.push(0);
	while(!q.empty()){
		int x=q.front(); q.pop();
		for(auto i:a){
			int p=x|i;
			if(p==np-1)return dis[x]+1;
			if(dis[p]>dis[x]+1){
				dis[p]=dis[x]+1;
				q.push(p);
			}
		}
	}
	return 0;
}
int solve(){ // 返回最小染色数
	mst(g,0);
	for(auto i:eset){
		int x=i.fi,y=i.se;
		g[x]|=1<<y;
		g[y]|=1<<x;
	}
	// 求所有独立集
	ind[0]=1;
	repeat(i,1,np){
		int w=63-__builtin_clzll(ll(i)); // 最高位
		if((g[w]&i)==0 && ind[i^(1<<w)])
			ind[i]=1;
	}
	// 删除所有不是极大独立集的独立集
	repeat(i,1,np)
	if(ind[i]){
		for(int j=1;j<np;j<<=1)
		if((i&j)==0 && ind[i|j]){
			ind[i]=0;
			break;
		}
		if(ind[i])
			a.push_back(i); // 记录极大独立集
	}
	return bfs();
}

仙人掌 using 圆方树

  • 仙人掌:每条边至多属于一个简单环的无向联通图
  • 圆方树:原来的点称为圆点,每个环新建一个方点,环上的圆点都与方点连接
  • 子仙人掌:以 r 为根,点 p 的子仙人掌是删掉 p 到 r 的所有简单路径后 p 所在的联通块。这个子仙人掌就是圆方树中以 r 为根时,p 子树中的所有圆点
  • 仙人掌的判定(DFS 树上差分),编号从哪开始都可以,\(O(n+m)\)
vector<int> a[N]; // vector<int> rec; // rec 存每个环的大小
bool vis[N]; int fa[N],lab[N],dep[N]; bool ans;
void dfs(int x){
	vis[x]=1;
	for(auto p:a[x])if(p!=fa[x]){
		if(!vis[p]){
			fa[p]=x; dep[p]=dep[x]+1;
			dfs(p); lab[x]+=lab[p];
		}
		else if(dep[p]<dep[x]){
			lab[x]++; lab[p]--;
			// rec.push_back(dep[x]-dep[p]+1);
		}
	}
	if(lab[x]>=2)ans=0;
}
bool iscactus(int s){
	fill(vis,vis+n+1,0);
	ans=1; fa[s]=-1; dfs(s); return ans;
}

二分图

二分图匹配 / 最大匹配

  • 匈牙利 / hungarian,左右顶点编号从 0 开始,\(O(VE)\)
vector<int> a[N]; // a: input, the left vertex x is connected to the right vertex a[x][i]
int dcnt,mch[N],dfn[N]; // mch: output, the right vertex p is connected to the left vertex mch[p]
bool dfs(int x){
	for(auto p:a[x]){
		if(dfn[p]!=dcnt){
			dfn[p]=dcnt;
			if(mch[p]==-1 || dfs(mch[p])){
				mch[p]=x;
				return 1;
			}
		}
	}
	return 0;
}
int hun(int n,int m){ // n,m: the number of the left/right vertexes. return max matching
	int ans=0;
	repeat(i,0,m)mch[i]=-1;
	repeat(i,0,n){
		dcnt++;
		if(dfs(i))ans++;
	}
	return ans;
}
  • HK算法 / Hopcroft-karp,左顶点编号从 0 开始,右顶点编号从 n开始,\(O(E\sqrt V)\)
vector<int> a[N]; // a: input, the left vertex x is connected to the right vertex a[x][i]
int mch[N*2],dep[N*2]; // mch: output, the vertex p is connected to the vertex mch[p] (p could be either left or right vertex)
bool bfs(int n,int m){
	static queue<int> q;
	fill(dep,dep+n+m,0);
	bool flag=0;
	repeat(i,0,n)if(mch[i]==-1)q.push(i);
	while(!q.empty()){
		int x=q.front(); q.pop();
		for(auto p:a[x]){
			if(!dep[p]){
				dep[p]=dep[x]+1;
				if(mch[p]==-1)flag=1;
				else dep[mch[p]]=dep[p]+1,q.push(mch[p]);
			}
		}
	}
	return flag;
}
bool dfs(int x){
	for(auto p:a[x]){
		if(dep[p]!=dep[x]+1) continue;
		dep[p]=0;
		if(mch[p]==-1 || dfs(mch[p])) {
			mch[x]=p; mch[p]=x;
			return 1;
		}
	}
	return 0;
}
int solve(int n,int m){ // n,m: the number of the left/right vertexes. return max matching
	int ans=0;
	fill(mch,mch+n+m,-1);
	while(bfs(n,m)){
		repeat(i,0,n)
		if(mch[i]==-1 && dfs(i))
			ans++;
	}
	return ans;
}
  • 网络流建图,编号从 0 开始,\(O(E\sqrt V)\)
int work(int n1,int n2,vector<pii> &eset){
	int n=n1+n2+2;
	int s=0,t=n1+n2+1;
	flow.init(n);
	repeat(i,1,n1+1)add(s,i,1);
	repeat(i,n1+1,n1+n2+1)add(i,t,1);
	for(const auto &i:eset){
		int x=i.fi,y=i.se;
		add(x+1,n1+y+1,1);
	}
	return flow.solve(s,t);
}

最大权匹配 using KM

  • 求满二分图的最大权匹配
  • 如果没有边就建零边,而且要求n<=m
  • 编号从 0 开始,\(O(n^3)\)
int e[N][N],n,m; // 邻接矩阵,左顶点数,右顶点数
int lx[N],ly[N]; // 顶标
int mch[N]; // 右顶点i连接的左顶点编号
bool fx[N],fy[N]; // 是否在增广路上
bool dfs(int i){
	fx[i]=1;
	repeat(j,0,n)
	if(lx[i]+ly[j]==e[i][j] && !fy[j]){
		fy[j]=1;
		if(mch[j]==-1 || dfs(mch[j])){
			mch[j]=i;
			return 1;
		}
	}
	return 0;
}
void update(){
	int fl=inf;
	repeat(i,0,n)if(fx[i])
	repeat(j,0,m)if(!fy[j])
		fl=min(fl,lx[i]+ly[j]-e[i][j]);
	repeat(i,0,n)if(fx[i])lx[i]-=fl;
	repeat(j,0,m)if(fy[j])ly[j]+=fl;
}
int solve(){ // 返回匹配数
	repeat(i,0,n){
		mch[i]=-1;
		lx[i]=ly[i]=0;
		repeat(j,0,m)
			lx[i]=max(lx[i],e[i][j]);
	}
	repeat(i,0,n)
	while(1){
		repeat(j,0,m)
			fx[j]=fy[j]=0;
		if(dfs(i))break;
		else update();
	}
	int ans=0;
	repeat(i,0,m)
	if(mch[i]!=-1)
		ans+=e[mch[i]][i];
	return ans;
}

稳定婚姻 using 延迟认可

  • 稳定意味着不存在一对不是情侣的男女,都认为当前伴侣不如对方
  • 编号从 0 开始,\(O(n^2)\)
struct node{
	int s[N]; // s的值给定
		// 对男生来说是女生编号排序
		// 对女生来说是男生的分数
	int now; // 选择的伴侣编号
}a[N],b[N]; // 男生,女生
int tr[N]; // 男生尝试表白了几次
queue<int> q; // 单身狗(男)排队
bool match(int x,int y){ // 配对,返回是否成功
	int x0=b[y].now;
	if(x0!=-1){
		if(b[y].s[x]<b[y].s[x0])
			return 0; // 分数不够,竞争失败
		q.push(x0);
	}
	a[x].now=y;
	b[y].now=x;
	return 1;
}
void stable_marriage(){ // 运行后a[].now,b[].now即结果
	q=queue<int>();
	repeat(i,0,n){
		b[i].now=-1;
		q.push(i);
		tr[i]=0;
	}
	while(!q.empty()){
		int x=q.front(); q.pop();
		int y=a[x].s[tr[x]++]; // 下一个最中意女生
		if(!match(x,y))
			q.push(x); // 下次努力
	}
}

一般图最大匹配 using 带花树

  • 对于一个无向图,找最多的边使得这些边两两无公共端点
  • 编号从 1 开始,\(O(n^3)\)
int n; DSU d;
deque<int> q; vector<int> e[N];
int mch[N],vis[N],dfn[N],fa[N],dcnt=0;
int lca(int x,int y){
	dcnt++;
	while(1){
		if(x==0)swap(x,y); x=d[x];
		if(dfn[x]==dcnt)return x;
		else dfn[x]=dcnt,x=fa[mch[x]];
	}
}
void shrink(int x,int y,int p){
	while(d[x]!=p){
		fa[x]=y; y=mch[x];
		if(vis[y]==2)vis[y]=1,q.push_back(y);
		if(d[x]==x)d[x]=p;
		if(d[y]==y)d[y]=p;
		x=fa[y];
	}
}
bool match(int s){
	d.init(n); fill(fa,fa+n+1,0);
	fill(vis,vis+n+1,0); vis[s]=1;
	q.assign(1,s);
	while(!q.empty()){
		int x=q.front(); q.pop_front();
		for(auto p:e[x]){
			if(d[x]==d[p] || vis[p]==2)continue;
			if(!vis[p]){
				vis[p]=2; fa[p]=x;
				if(!mch[p]){
					for(int now=p,last,tmp;now;now=last){
						last=mch[tmp=fa[now]];
						mch[now]=tmp,mch[tmp]=now;
					}
					return 1;
				}
				vis[mch[p]]=1; q.push_back(mch[p]);
			}
			else if(vis[p]==1){
				int l=lca(x,p);
				shrink(x,p,l);
				shrink(p,x,l);
			}
		}
	}
	return 0;
}
int solve(){ // 返回匹配数,mch[]是匹配结果(即匹配x和mch[x]),==0表示不匹配
	int ans=0; fill(mch,mch+n+1,0);
	repeat(i,1,n+1)ans+=(!mch[i] && match(i));
	return ans;
}

一般图最大权匹配 using 带权带花树

  • 它带什么花跟我有什么关系。
  • 编号从 1 开始,\(O(n^3)\)
struct edge{int u,v,w;};
int n,n_x;
edge g[N*2][N*2];
int lab[N*2];
int match[N*2],slack[N*2],st[N*2],pa[N*2];
int flower_from[N*2][N],S[N*2],vis[N*2];
vector<int> flower[N*2];
queue<int> q;
int e_delta(const edge &e){
	return lab[e.u]+lab[e.v]-g[e.u][e.v].w*2;
}
void update_slack(int u,int x){
	if(!slack[x] || e_delta(g[u][x])<e_delta(g[slack[x]][x]))slack[x]=u;
}
void set_slack(int x){
	slack[x]=0;
	repeat(u,1,n+1)
		if(g[u][x].w>0 && st[u]!=x && S[st[u]]==0)update_slack(u,x);
}
void q_push(int x){
	if(x<=n)q.push(x);
	else repeat(i,0,flower[x].size())q_push(flower[x][i]);
}
void set_st(int x,int b){
	st[x]=b;
	if(x>n)repeat(i,0,flower[x].size())
			set_st(flower[x][i],b);
}
int get_pr(int b,int xr){
	int pr=find(flower[b].begin(),flower[b].end(),xr)-flower[b].begin();
	if(pr%2==1){
		reverse(flower[b].begin()+1,flower[b].end());
		return (int)flower[b].size()-pr;
	}else return pr;
}
void set_match(int u,int v){
	match[u]=g[u][v].v;
	if(u>n){
		edge e=g[u][v];
		int xr=flower_from[u][e.u],pr=get_pr(u,xr);
		for(int i=0;i<pr;++i)set_match(flower[u][i],flower[u][i^1]);
		set_match(xr,v);
		rotate(flower[u].begin(),flower[u].begin()+pr,flower[u].end());
	}
}
void augment(int u,int v){
	while(1){
		int xnv=st[match[u]];
		set_match(u,v);
		if(!xnv)return;
		set_match(xnv,st[pa[xnv]]);
		u=st[pa[xnv]],v=xnv;
	}
}
int get_lca(int u,int v){
	static int t=0;
	for(++t;u || v;swap(u,v)){
		if(u==0)continue;
		if(vis[u]==t)return u;
		vis[u]=t;
		u=st[match[u]];
		if(u)u=st[pa[u]];
	}
	return 0;
}
void add_blossom(int u,int lca,int v){
	int b=n+1;
	while(b<=n_x && st[b])++b;
	if(b>n_x)++n_x;
	lab[b]=0,S[b]=0;
	match[b]=match[lca];
	flower[b].clear();
	flower[b].push_back(lca);
	for(int x=u,y;x!=lca;x=st[pa[y]])
		flower[b].push_back(x),flower[b].push_back(y=st[match[x]]),q_push(y);
	reverse(flower[b].begin()+1,flower[b].end());
	for(int x=v,y;x!=lca;x=st[pa[y]])
		flower[b].push_back(x),flower[b].push_back(y=st[match[x]]),q_push(y);
	set_st(b,b);
	repeat(x,1,n_x+1)g[b][x].w=g[x][b].w=0;
	repeat(x,1,n+1)flower_from[b][x]=0;
	repeat(i,0,flower[b].size()){
		int xs=flower[b][i];
		for(int x=1;x<=n_x;++x)
			if(g[b][x].w==0 || e_delta(g[xs][x])<e_delta(g[b][x]))
				g[b][x]=g[xs][x],g[x][b]=g[x][xs];
		for(int x=1;x<=n;++x)
			if(flower_from[xs][x])flower_from[b][x]=xs;
	}
	set_slack(b);
}
void expand_blossom(int b){ // S[b] == 1
	for(int i=0;i<(int)flower[b].size();++i)
		set_st(flower[b][i],flower[b][i]);
	int xr=flower_from[b][g[b][pa[b]].u],pr=get_pr(b,xr);
	for(int i=0;i<pr;i+=2){
		int xs=flower[b][i],xns=flower[b][i+1];
		pa[xs]=g[xns][xs].u;
		S[xs]=1,S[xns]=0;
		slack[xs]=0,set_slack(xns);
		q_push(xns);
	}
	S[xr]=1,pa[xr]=pa[b];
	for(int i=pr+1;i<(int)flower[b].size();++i){
		int xs=flower[b][i];
		S[xs]=-1,set_slack(xs);
	}
	st[b]=0;
}
bool on_found_edge(const edge &e){
	int u=st[e.u],v=st[e.v];
	if(S[v]==-1){
		pa[v]=e.u,S[v]=1;
		int nu=st[match[v]];
		slack[v]=slack[nu]=0;
		S[nu]=0,q_push(nu);
	}else if(S[v]==0){
		int lca=get_lca(u,v);
		if(!lca)return augment(u,v),augment(v,u),1;
		else add_blossom(u,lca,v);
	}
	return 0;
}
bool matching(){
	memset(S+1,-1,sizeof(int)*n_x);
	memset(slack+1,0,sizeof(int)*n_x);
	q=queue<int>();
	for(int x=1;x<=n_x;++x)
		if(st[x]==x && !match[x])pa[x]=0,S[x]=0,q_push(x);
	if(q.empty())return false;
	while(1){
		while(q.size()){
			int u=q.front(); q.pop();
			if(S[st[u]]==1)continue;
			repeat(v,1,n+1)
				if(g[u][v].w>0 && st[u]!=st[v]){
					if(e_delta(g[u][v])==0){
						if(on_found_edge(g[u][v]))return true;
					}else update_slack(u,st[v]);
				}
		}
		int d=inf;
		for(int b=n+1;b<=n_x;++b)
			if(st[b]==b && S[b]==1)d=min(d,lab[b]/2);
		repeat(x,1,n_x+1)
			if(st[x]==x && slack[x]){
				if(S[x]==-1)d=min(d,e_delta(g[slack[x]][x]));
				else if(S[x]==0)d=min(d,e_delta(g[slack[x]][x])/2);
			}
		repeat(u,1,n+1){
			if(S[st[u]]==0){
				if(lab[u]<=d)return 0;
				lab[u]-=d;
			}else if(S[st[u]]==1)lab[u]+=d;
		}
		for(int b=n+1;b<=n_x;++b)
			if(st[b]==b){
				if(S[st[b]]==0)lab[b]+=d*2;
				else if(S[st[b]]==1)lab[b]-=d*2;
			}
		q=queue<int>();
		repeat(x,1,n_x+1)
			if(st[x]==x && slack[x] && st[slack[x]]!=x && e_delta(g[slack[x]][x])==0)
				if(on_found_edge(g[slack[x]][x]))return true;
		for(int b=n+1;b<=n_x;++b)
			if(st[b]==b && S[b]==1 && lab[b]==0)expand_blossom(b);
	}
	return false;
}
pair<ll,int> weight_blossom(){
	memset(match+1,0,sizeof(int)*n);
	n_x=n;
	int n_matches=0;
	ll tot_weight=0;
	for(int u=0;u<=n;++u)st[u]=u,flower[u].clear();
	int w_max=0;
	repeat(u,1,n+1)
	repeat(v,1,n+1){
		flower_from[u][v]=(u==v?u:0);
		w_max=max(w_max,g[u][v].w);
	}
	repeat(u,1,n+1)lab[u]=w_max;
	while(matching())++n_matches;
	repeat(u,1,n+1)
		if(match[u] && match[u]<u)
			tot_weight+=g[u][match[u]].w;
	return make_pair(tot_weight,n_matches);
}
void init_weight_graph(){
	repeat(u,1,n+1)
		repeat(v,1,n+1)
			g[u][v]=edge{u,v,0};
}
void Solve(){
	int m;
	scanf("%d%d",&n,&m);
	init_weight_graph();
	repeat(i,0,m){
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		g[u][v].w=g[v][u].w=w;
	}
	printf("%lld\n",weight_blossom().first);
	repeat(u,1,n+1)printf("%d ",match[u]); puts("");
}

网络流

最大流 using Dinic

  • 编号从 0 开始,\(O(V^2E)\)
struct FLOW{
	struct edge{int to,w,nxt;};
	vector<edge> a; int head[N],cur[N];
	int n,s,t;
	queue<int> q; bool inque[N];
	int dep[N];
	void ae(int x,int y,int w){ // add edge
		a.push_back({y,w,head[x]});
		head[x]=a.size()-1;
	}
	bool bfs(){ // get dep[]
		fill(dep,dep+n,inf); dep[s]=0;
		copy(head,head+n,cur);
		q=queue<int>(); q.push(s);
		while(!q.empty()){
			int x=q.front(); q.pop(); inque[x]=0;
			for(int i=head[x];i!=-1;i=a[i].nxt){
				int p=a[i].to;
				if(dep[p]>dep[x]+1 && a[i].w){
					dep[p]=dep[x]+1;
					if(inque[p]==0){
						inque[p]=1;
						q.push(p);
					}
				}
			}
		}
		return dep[t]!=inf;
	}
	int dfs(int x,int flow){ // extend
		int now,ans=0;
		if(x==t)return flow;
		for(int &i=cur[x];i!=-1;i=a[i].nxt){
			int p=a[i].to;
			if(a[i].w && dep[p]==dep[x]+1)
			if((now=dfs(p,min(flow,a[i].w)))){
				a[i].w-=now;
				a[i^1].w+=now;
				ans+=now,flow-=now;
				if(flow==0)break;
			}
		}
		return ans;
	}
	void init(int _n){
		n=_n+1; a.clear();
		fill(head,head+n,-1);
		fill(inque,inque+n,0);
	}
	int solve(int _s,int _t){ // return max flow
		s=_s,t=_t;
		int ans=0;
		while(bfs())ans+=dfs(s,inf);
		return ans;
	}
}flow;
void add(int x,int y,int w){flow.ae(x,y,w),flow.ae(y,x,0);}
// 先flow.init(n),再add添边,最后flow.solve(s,t)

最小费用最大流 using MCMF

  • 费用流一般指最小费用最大流(最大费用最大流把费用取反即可)
  • 编号从 0 开始,\(O(VE^2)\)
struct FLOW{
	struct edge{int to,w,cost,nxt;};
	vector<edge> a; int head[N];
	int n,s,t,totcost;
	deque<int> q;
	bool inque[N];
	int dis[N];
	struct{int to,e;}pre[N];
	void ae(int x,int y,int w,int cost){
		a.push_back((edge){y,w,cost,head[x]});
		head[x]=a.size()-1;
	}
	bool spfa(){
		fill(dis,dis+n,inf); dis[s]=0;
		q.assign(1,s);
		while(!q.empty()){
			int x=q.front(); q.pop_front();
			inque[x]=0;
			for(int i=head[x];i!=-1;i=a[i].nxt){
				int p=a[i].to;
				if(dis[p]>dis[x]+a[i].cost && a[i].w){
					dis[p]=dis[x]+a[i].cost;
					pre[p]={x,i};
					if(inque[p]==0){
						inque[p]=1;
						if(!q.empty()
						&& dis[q.front()]<=dis[p])
							q.push_back(p);
						else q.push_front(p);
					}
				}
			}
		}
		return dis[t]!=inf;
	}
	void init(int _n){
		n=_n+1;
		a.clear();
		fill(head,head+n,-1);
		fill(inque,inque+n,0);
	}
	int solve(int _s,int _t){ // 返回最大流,费用存totcost里
		s=_s,t=_t;
		int ans=0;
		totcost=0;
		while(spfa()){
			int fl=inf;
			for(int i=t;i!=s;i=pre[i].to)
				fl=min(fl,a[pre[i].e].w);
			for(int i=t;i!=s;i=pre[i].to){
				a[pre[i].e].w-=fl;
				a[pre[i].e^1].w+=fl;
			}
			totcost+=dis[t]*fl;
			ans+=fl;
		}
		return ans;
	}
}flow;
void add(int x,int y,int w,int cost){
	flow.ae(x,y,w,cost),flow.ae(y,x,0,-cost);
}
// 先 flow.init(n),再 add 添边,最后 flow.solve(s,t)

图论杂项

Kruskal 重构树

  • 基于 Kruskal 算法构建的有根树。
  • 在原图中从某一点出发,只走边权不超过 w 的边,可达的点集在重构树中是一棵子树,对应 DFS 序的一个区间。
  • 构建过程:边权从小到大访问边 (x, y)。如果 x, y 不连通,新建点 s 连接 x, y 所在树的根,且 s 为新树的根,s 的点权为边 (x, y) 的边权。
  • 查找 x 能访问的点时,树上倍增找到最远的点权不大于 w 的祖先。
  • 性质:二叉树,大根堆。
  • luogu P4197,编号从 1 开始,\(O(E\log E)\)
DSU d;
vector<int> a[N];
int h[N],w[N];
vector<array<int,3>> eset;
vector<int> rec; int l[N],r[N],fa[N][logN];
void dfs(int x){
	l[x]=rec.size(); rec.push_back(x);
	for(auto p:a[x])fa[p][0]=x,dfs(p);
	r[x]=rec.size()-1;
}
divtree tr; // 其中一行改为 repeat(i,1,n+1)tr[0][i]=a[i]=h[rec[i]];
void kru(){
	sort(eset.begin(),eset.end(),
		[](array<int,3> &a,array<int,3> &b){
			return a[2]<b[2];
		}
	);
	int s=n;
	for(auto i:eset){
		int x=d[i[0]],y=d[i[1]]; if(x==y)continue;
		++s; a[s].push_back(x); a[s].push_back(y);
		w[s]=i[2];
		d[x]=d[y]=s;
	}
	++s; repeat(i,1,s+1-1)if(d[i]==i)a[s].push_back(i);
	w[s]=inf;
}
void Solve(){
	int n=read(),m=read(),q=read(); d.init(n*2); rec.assign(1,0);
	repeat(i,1,n+1)h[i]=read();
	while(m--){
		int x=read(),y=read(),w=read();
		eset.push_back({x,y,w});
	}
	kru(); // get kruskal tree
	fa[s][0]=s; dfs(s); // get DFS order & fa[i][0]
	repeat(i,1,logN)
	repeat(x,1,s+1)
		fa[x][i]=fa[fa[x][i-1]][i-1];
	tr.init(s);
	while(q--){
		int x=read(),ww=read(),k=read();
		repeat_back(i,0,logN)
			if(w[fa[x][i]]<=ww)x=fa[x][i];
		if((r[x]-l[x]+1)/2+1<k)puts("-1");
		else printf("%d\n",tr.maxk(l[x],r[x],k)); 
	}
}

DSU 重构树

  • 离线处理连边和连通块询问
  • 原理:DSU 重构树的 DFS 序保证任意时刻的任意连通块是一个区间
  • 接口:先读入 ops,然后 build(n),然后依次 merge(x, y)(要保证顺序不变)。询问连通块区间 query(x, l, r),询问结点 x 在 DFS 序中的位置是 red.l[x]
  • 编号从 1 开始
struct dsu_rebuilder{
	int cnt,l[N],r[N];
	DSU d;
	vector<int> a[N];
	vector<pii> ops; // input
	void dfs(int x){ // private
		l[x]=r[x]=cnt++;
		for(auto p:a[x])dfs(p);
	}
	void build(int n){
		cnt=0; d.init(n);
		repeat(i,0,n+1)a[i].clear();
		for(auto i:ops){
			int x=i.first,y=i.second;
			x=d[x]; y=d[y];
			if(x!=y){
				a[x].push_back(y);
				d[y]=d[x];
			}
		}
		repeat(i,1,n+1)if(d[i]==i)a[0].push_back(i);
		dfs(0); d.init(n); ops.clear();
	}
	void merge(int x,int y){
		x=d[x]; y=d[y];
		if(x!=y)d[y]=d[x],r[x]=r[y];
	}
	void query(int x,int &L,int &R){
		x=d[x]; L=l[x]; R=r[x];
	}
}red;
posted @ 2021-07-07 01:15  axiomofchoice  阅读(204)  评论(0编辑  收藏  举报