第一周学习总结

发布时间 2024-01-11 17:11:10作者: gevenfeng

第一周学习总结

二分图

定义

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

图例:

判定——染色法

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

染色结束后,如果能实现所有 相邻顶点的颜色都不同,则 \(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;
}