第一周学习总结

第一周学习总结

二分图

定义

若 \(G\) 是一个无向图,\(G\) 的顶点分成 \(X\) 和 \(Y\) 两部分,\(G\) 中每条边的两个顶点一定是 一个属于 \(X\) 另一个属于 \(Y\),则称图 \(G\) 为 二分图。

图例:

![]( https://cdn.luogu.com.cn/upload/image_hosting/l2hrkqhb.png?x-oss-process=image/resize ,m_lfit,h_170,w_225)

判定——染色法

用两种颜色对所有顶点染色,要求一条边所连接的 相邻顶点的颜色不同。

染色结束后,如果能实现所有 相邻顶点的颜色都不同,则 \(G\) 就是一个二分图。

如果 \(G\) 不是一个二分图,则说明 \(G\) 中 存在奇环。

二分图的最大匹配

增广路径:若 \(P\) 是图 \(G\) 中一条连通两个 未匹配顶点 的路径,并且属匹配边集 \(M\) 的边和不属 \(M\) 的边(即已匹配和未匹配的边)在 \(P\) 上 交替出现,则称 \(P\) 为相对于 \(M\) 的一条 增广路径。

匈牙利算法

算法过程:不断在图中寻找增广路径,每找到一条连接两个 未盖点 之间的增广路径,就将增广路径上的边 取反(将增广路径上 本属于匹配边的边 从最大匹配中 删除,本 不属于匹配边的边加入 最大匹配)。

由于增广路径的特殊性质,在每次取反操作后,最大匹配的边数会 加1。

证明:由于增广路径的两端点都是未盖点,所以连接两端点的边原先 都不属于最大匹配。所以,增广路径上本不属于最大匹配的边比原属于最大匹配的边数量 多1。

代码实现
#include<stdio.h>
#include<vector>
#include<cstring>
const int N=505;
std::vector<int> v[N];
int n,m,match[N],p[N];
bool vis[N];
bool dfs(int u){
	for(auto i:v[u]){
		if(!vis[i]){
			vis[i]=true;
			if(!match[i]||dfs(match[i])){
				match[i]=u;
				return true;
			}
		}
	}
	return false;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1,x;i<=n;i++){
		scanf("%d",&x);
		int y;
		while(x--) scanf("%d",&y),v[i].emplace_back(y);
	}
	int ans=0;
	for(int i=1;i<=n;i++){
		std::memset(vis,false,sizeof vis);
		ans+=dfs(i);
	}
	printf("%d",ans);
	return 0;
}

时间复杂度:\(O(nm)\)。

最大流

由于每个点只会 匹配一个点,所以可以用最大流模型转化求解。

设一个超级源点 \(s\) 和一个超级汇点 \(t\)。每次加一条边 \((x,y)\),我们都建 从 \(x\) 到 \(y\) 的一条流量为 \(1\) 的边。

在跑最大流之前,我们再 把 \(s\) 向所有属于 \(V_1\) 的点建一条流量为 \(1\) 的边,把所有属于 \(V_2\) 的点向 \(t\) 建一条流量为 \(1\) 的边。

最后使用最大流算法(如 \(EK\),\(Dinic\),\(ISAP\),\(HLPP\) 等)求解,最大流量即为二分图的最大匹配边数。

注意:最大流在建边时需建一条 流量为 \(0\) 的反向边。

代码实现
#include<stdio.h>//Dinic
#include<algorithm>
#include<queue>
const int N=1e6+5,inf=0x3f3f3f3f;
struct jie{int to,val,nxt;}a[N];
int n,m,e,s,t,head[N],cnt=1,now[N],depth[N];
void add(int x,int y,int z){
	cnt++;
	a[cnt].to=y;
	a[cnt].val=z;
	a[cnt].nxt=head[x];
	head[x]=cnt;
}
bool bfs(){
	for(int i=1;i<=n+m+2;i++) depth[i]=inf;
	std::queue<int> q;
	depth[s]=0,now[s]=head[s],q.push(s);
	while(!q.empty()){
		int u=q.front();q.pop();
		for(int i=head[u];i;i=a[i].nxt){
			if(a[i].val>0&&depth[a[i].to]==inf){
				now[a[i].to]=head[a[i].to],depth[a[i].to]=depth[u]+1,q.push(a[i].to);
				if(a[i].to==t) return true;
			}
		}
	}
	return false;
}
int dfs(int u,int sum){
	if(u==t) return sum;
	int k,flow=0;
	for(int i=now[u];i&&sum>0;i=a[i].nxt){
		now[u]=i;
		if(a[i].val>0&&depth[a[i].to]==depth[u]+1){
			k=dfs(a[i].to,std::min(sum,a[i].val));
			if(!k) depth[a[i].to]=inf;
			a[i].val-=k,a[i^1].val+=k;
			flow+=k,sum-=k;
		}
	}
	return flow;
}
int Dinic(){
	int ans=0;
	while(bfs()) ans+=dfs(s,inf);
	return ans;
}
int main(){
	scanf("%d%d%d",&n,&m,&e);
	s=n+m+1,t=n+m+2;
	for(int i=1;i<=n;i++) add(s,i,1),add(i,s,0);
	for(int i=1;i<=m;i++) add(i+n,t,1),add(t,i+n,0);
	int x,y;
	while(e--) scanf("%d%d",&x,&y),add(x,y+n,1),add(y+n,x,0);
	printf("%d",Dinic());
	return 0;
}
/*
How can I use Dinic to solve it?
e.g.:

scanf("%d%d%d",&n,&m,&e);
s=n+m+1,t=n+m+2;
for(int i=1;i<=n;i++) add(s,i,1),add(i,s,0);
for(int i=1;i<=m;i++) add(i+n,t,1),add(t,i+n,0);
int x,y;
while(e--) scanf("%d%d",&x,&y),add(x,y+n,1),add(y+n,x,0);
printf("%d",Dinic());

warning:the number of 'cnt' must start of 1
*/

时间复杂度:\(O(\sqrt{n} \cdot m)\)。

关于二分图的扩展定理

\(1.\) 二分图的 最大独立集=顶点数 \(-\) 二分图的最大匹配数;

\(2.\) 二分图的 最小顶点覆盖=二分图的 最大匹配数;

\(3.\) 二分图的 最小路径覆盖=顶点数 \(-\) 二分图的最大匹配数。

好题

[NOI2009]变换序列

分情况进行讨论。

\(1.\) \(\lvert i-T_i\rvert = d\)

此时 \(T_i=i \pm d\)。

\(2.\) \(N- \lvert i-T_i \rvert=d\)

此时 \(T_i=i-d+N\) 或 \(T_i=i+d-N\)。

由于 \(i+d \equiv i+d-N \pmod{N}\),\(i-d \equiv i-d+N \pmod{N}\),所以只用讨论 \(i \pm d\) 这两种情况。

要同时满足字典序最小,则要满足以下条件:

\(1.\) 建边时 字典序小的在前面。

\(2.\) 定理:当每个顶点所连接的边只有 不超过两条 时,倒序跑匈牙利算法字典序会最小。

按照上述条件实现即可。

[HNOI2013 DAY1] 消毒

如果这个题是在二维平面上进行的话,那这个题就是一个二分图的 最小点覆盖 模板题。

现在考虑如何转化为三维问题。

由于不会三分图,所以我们考虑将长方体 拍扁,即把三维问题转化为 二维问题。

易知每层有两种选择:

\(1.\) 将自己这一层 全部消毒。

\(2.\) 与其它层 一起消毒。

所以,我们枚举每一层是否单独处理。如果是,则直接将答案加 \(1\),否则我们把剩下层的点 压到平面上,用 最小点覆盖 求解。

最后将每种情况答案取最小值即为最终答案。

连通性相关

连通图

在无向图中,任意两点都相互可达,则称该图为连通图。

连通分量

无向图的 最大 连通子图称为无向图的连通分量。

最大连通子图

该子图是原图的 连通子图,将原图的任意一个不在该子图中的点加入到该子图后,该子图 不再连通。

有向图的连通性

在有向图中,对于任意一对顶点 \(u,v\) 都存在 从 \(u\) 到 \(v\) 和从 \(v\) 到 \(u\) 的路径,则称此图为 强连通图。

有向图的 最大 强连通子图称为该图的 强连通分量(\(\operatorname{SCC}\),\(\operatorname{Strong}\) \(\operatorname{Connected}\) \(\operatorname{Component}\))。

最大强连通子图

该子图是原图的 强连通子图,将原图中任意一个不在该子图内的点加入到该子图后,该子图 不再强连通。

求强连通分量

传递闭包

时间复杂度为 \(O(n^3)\)。

\(\operatorname{Kosaraju}\) 算法

算法过程:

\((1)\) 在原图上做一次 \(\operatorname{DFS}\),标记点的先后顺序。在此过程中,标记所有经过的点:把递归到 最底层的那个点标记为最小,然后在回溯过程中,其他点的标记 逐个递增。

\((2)\) 在反图上再做一次 \(\operatorname{DFS}\),顺序 从标记最大的点开始,到标记最小的点。每次遍历到的点 都在同一个 \(\operatorname{SCC}\) 里。

代码:

#include<stdio.h>
#include<vector>
#include<cstring>
const int N=1e5+5;
std::vector<int> S,v[N],revv[N];
int n,m,sccno[N],cnt;
bool vis[N];
void dfs1(int u){
    if(vis[u]) return;
    vis[u]=true;
    for(auto i:v[u]) dfs1(i);
    S.emplace_back(u); 
}
void dfs2(int u){
    if(sccno[u]) return;
    sccno[u]=cnt;
    for(auto i:revv[u]) dfs2(i);
}
void kosaraju(){
    std::memset(vis,false,sizeof vis);
    std::memset(sccno,0,sizeof sccno);
    S.clear(),cnt=0;
    for(int i=1;i<=n;i++) dfs1(i);
    for(auto it=S.rbegin();it!=S.rend();it++) if(!sccno[*it]) cnt++,dfs2(*it); 
}
int main(){
    while(~scanf("%d%d",&n,&m),n|m){
        for(int i=1;i<=n;i++) v[i].clear(),revv[i].clear();
        int x,y;
        while(m--) scanf("%d%d",&x,&y),v[x].emplace_back(y),revv[y].emplace_back(x);
        kosaraju();
        puts(cnt==1? "Yes":"No");
    }
    return 0;
}

时间复杂度:\(O(n+m)\)。

但由于要做两次 \(\operatorname{DFS}\),所以没有 \(\operatorname{Tarjan}\) 算法快。

\(\operatorname{Tarjan}\) 算法

用 \(num_i\) 表示点 \(i\) 的 访问次序。

用 \(low_i\) 表示点 \(i\) 在搜索树上 能返回的最远祖先节点。

每次用自己在原图上的儿子节点来更新自身的 \(low\) 值。

在完成自身的更新后,如果 \(num_i=low_i\),说明以 \(i\) 为根的子树内的节点 都在同一强连通分量里。

如何记录节点 \(i\) 所在的强连通分量标号?由于递归的性质,用 栈 存储即可。

代码:

#include<stdio.h>
#include<algorithm>
#include<vector>
#include<utility>
#include<cstring>
#define x first
#define y second
const int N=1e4+5;
std::vector<int> v[N];
std::vector<std::pair<int,int> > e;
int n,cnt,low[N],num[N],dfn,sccno[N],s[N],top;
bool vis[N];
void dfs(int u){
    s[++top]=u;
    low[u]=++dfn,num[u]=dfn;
    for(auto i:v[u]){
        if(!num[i]) dfs(i),low[u]=std::min(low[u],low[i]);
        else if(!sccno[i]) low[u]=std::min(low[u],num[i]);
    }
    if(low[u]==num[u]){
        cnt++;
        while(1){
            int i=s[top--];
            sccno[i]=cnt;
            if(u==i) break;
        }
    }
}
void tarjan(){
    std::memset(sccno,0,sizeof sccno);
    std::memset(num,0,sizeof num);
    std::memset(low,0,sizeof low);
    cnt=0,top=0,dfn=0;
    for(int i=1;i<=n;i++) if(!num[i]) dfs(i);
}
int main(){
    scanf("%d",&n);
    for(int i=1,x;i<=n;i++){
    	for(int j=1;j<=n;j++){
    		scanf("%d",&x);
    		if(x) v[i].emplace_back(j),e.emplace_back(std::make_pair(i,j));
		}
	}
	tarjan();
	for(auto i:e) if(sccno[i.x]!=sccno[i.y]) vis[sccno[i.y]]=true;
	int ans=0;
	for(int i=1;i<=cnt;i++) ans+=!vis[i];
	printf("%d",ans);
    return 0;
}

时间复杂度为 \(O(n+m)\)。

有关无向图的概念

\(k\) 连通图:在任意两个顶点之间 至少存在 \(k\) 条不相交路径 的图。

双连通图:在任意两个顶点之间 至少存在 \(2\) 条不相交路径 的图。

割点:是无向连通图中一个顶点 \(u\), 如果删除它以及它关联的边后,得到的新图 至少包含两个连通分量。

桥:无向连通图中的一条边,如果删除它,得到的新图 包含两个连通分量。

双连通图:不含割点 的无向连通图。

双连通分量:无向连通图的 最大双连通子图。

点双连通分量:通过 找割点 获得的双连通分量。

边双连通分量:通过 找桥 获得的双连通分量。

求割点

还是需要处理一下每个节点的 \(low\) 值和 \(num\) 值。

点 \(u\) 是 割点,当且仅当在搜索树上:

\((1)\) \(u\) 是 树根,且子树数量 不少于 \(2\);

\((2)\) \(u\) 不是树根,且存在边 \((u,v)\),使得 \(num_u<low_v\)。

代码:

#include<stdio.h>
#include<algorithm>
#include<vector>
#include<cstring>
const int N=1e5+5;
std::vector<int> v[N];
int n,m,low[N],num[N],dfn,root;
bool iscut[N];
void dfs(int u,int fa){
	low[u]=++dfn,num[u]=dfn;
	int child=0;
	for(auto i:v[u]){
		if(!num[i]){
			child++;
			dfs(i,u);
			low[u]=std::min(low[u],low[i]);
			if(u!=root&&low[i]>=num[u]) iscut[u]=true;
		}
		else if(i!=fa&&num[i]<num[u]) low[u]=std::min(low[u],num[i]);
	}
	if(u==root&&child>1) iscut[u]=true;
}
void tarjan(){
	dfn=0;
	std::memset(low,0,sizeof low);
	std::memset(num,0,sizeof num);
	std::memset(iscut,false,sizeof iscut);
	for(int i=1;i<=n;i++) if(!num[i]) root=i,dfs(i,-1);
}
void tarjan(int start){
	root=start;
	dfs(start,-1);
}
int main(){
	scanf("%d%d",&n,&m);
	int x,y;
	while(m--) scanf("%d%d",&x,&y),v[x].emplace_back(y),v[y].emplace_back(x);
	tarjan();
	int cnt=0;
	for(int i=1;i<=n;i++) cnt+=iscut[i];
	printf("%d\n",cnt);
	for(int i=1;i<=n;i++) if(iscut[i]) printf("%d ",i);
	return 0;
}

求桥

一条在搜索树上的无向边 \((u,v)\) 是桥,当且仅当 \(num_u<low_v\)。

代码:

void dfs(int u,int fa){
	low[u]=++dfn,num[u]=dfn;
//	int child=0;
	for(auto i:v[u]){
		if(!num[i]){
//			child++;
			dfs(i,u);
			low[u]=std::min(low[u],low[i]);
			if(low[i]>num[u]) iscut[std::make_pair(u,i)]=true,iscut[std::make_pair(i,u)]=true;
		}
		else if(i!=fa&&num[i]<num[u]) low[u]=std::min(low[u],num[i]);
	}
//	if(u==root&&child>1) iscut[u]=true;
}
posted @ 2024-01-11 17:10  gevenfeng  阅读(47)  评论(0)    收藏  举报