大枝江、Graph Subset Problem

最近最大团的题目出现得越来越多,想到之前模拟赛的大枝江还没订,有点慌。

大枝江

\(n\) 个人站成一排,最开始 \(i\) 站在 \(i\) 处。接下来进行 \(m\) 次操作,每次操作让 \(x_i\)\(x_i+1\) 握手,并交换其位置。求握手关系构成的图的最大团。\(n,m\le 2\times 10^5\)

突破口:不握手关系按编号顺序构成传递闭包,即 \(\forall i<j<k,\)\(i,j\) 没握手、\(j,k\) 没握手,则 \(i,k\) 没握手。

由于最大团 = 补图的最大独立集 = 补图定向后的最小链覆盖,因此答案就是 \(G'=(V'=V,E'=\{(u,v)\notin E \mid u<v\})\) 的拆点二分图最大匹配。(注:代码为了实现方便,是 \(u>v\) 定向的。)

先来介绍一个更快的匈牙利,在 \(G=(V,E)\) 的补图上跑时,总复杂度是 \(O(VE\alpha)\) 的。具体就是用并查集维护下一个没有 vis 的点。(我好像不是很会分析匈牙利复杂度……)

//事先有:for(int i=1;i<=n;i++)sort(G[i].begin(),G[i].end());
bool dfs(int x){
	int k=0;
	for(int j=find(1);j<x;j=find(j+1)){
		while(k<G[x].size()&&G[x][k]<j)k++;
		if(k<G[x].size()&&G[x][k]==j)continue;
		fa[j]=j+1;
		if(!matr[j]||dfs(matr[j])){
			matr[j]=x,matl[x]=j;
			return 1;
		}
	}
	return 0;
}

下面考虑怎么实现拆点二分图最大匹配。这个二分图的边数太多了,不能直接跑匈牙利。考虑可以先尽量匹配,即对于每个点,找到其邻居中尚未匹配的任意一个点,匹配上这条边。这与匈牙利最大的不同是,不用每次清空了。这部分代码:

for(int i=1;i<=n;i++)sort(G[i].begin(),G[i].end());
for(int i=1;i<=n+1;i++)fa[i]=i;
for(int i=1;i<=n;i++){
	int k=0;
	for(int j=find(1);j<i;j=find(j+1)){
		while(k<G[i].size()&&G[i][k]<j)k++;
		if(k<G[i].size()&&G[i][k]==j)continue;
		fa[j]=j+1;
		matl[i]=j;
		matr[j]=i;
		ans++;
		break;
	}
}

那么剩余没有匹配上的左部点数量是多少呢?由于最开始左部点和右部点都是 \(n\) 个,假如现在两边都还剩 \(k\) 个匹配不上,说明不存在任意一条从左边的 \(k\) 个之一连向右边的 \(k\) 个之一的边,而这就有 \(k\times k\) 条边,那么由于这是在补图上考虑,在原图上就对应了这 \(k\times k\) 条边全都有,由于原图只有 \(m\) 条边,所以 \(k\le \sqrt m\)
于是我们可以对剩下的 \(O(\sqrt m)\) 个点跑匈牙利,这部分复杂度就是 \(O(\sqrt m\times m)\) 的。

完整代码:

#include <bits/stdc++.h>
using namespace std;
inline int read(){
	int x=0;char ch=getchar();
	while(ch<'0'||ch>'9')ch=getchar();
	while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
	return x;
}
const int N=2e5+5;
int n,m,ans,aa[N],fa[N],matl[N],matr[N];
vector<int>G[N];
int find(int x){
	return x==fa[x]?x:fa[x]=find(fa[x]);
}
bool dfs(int x){
	int k=0;
	for(int j=find(1);j<x;j=find(j+1)){
		while(k<G[x].size()&&G[x][k]<j)k++;
		if(k<G[x].size()&&G[x][k]==j)continue;
		fa[j]=j+1;
		if(!matr[j]||dfs(matr[j])){
			matr[j]=x,matl[x]=j;
			return 1;
		}
	}
	return 0;
}
int main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++)aa[i]=i;
	for(int i=1,x;i<=m;i++){
		x=read();
		if(aa[x]>aa[x+1])G[aa[x]].emplace_back(aa[x+1]);
		else G[aa[x+1]].emplace_back(aa[x]);
		swap(aa[x],aa[x+1]);
	}
	for(int i=1;i<=n;i++)sort(G[i].begin(),G[i].end());
	//max clique = butu max duliji = n - butu chaidian bipartite max matching
	for(int i=1;i<=n+1;i++)fa[i]=i;
	for(int i=1;i<=n;i++){
		int k=0;
		for(int j=find(1);j<i;j=find(j+1)){
			while(k<G[i].size()&&G[i][k]<j)k++;
			if(k<G[i].size()&&G[i][k]==j)continue;
			fa[j]=j+1;
			matl[i]=j;
			matr[j]=i;
			ans++;
			break;
		}
	}
	for(int i=1;i<=n;i++)if(!matl[i]){
		for(int j=1;j<=n+1;j++)fa[j]=j;
		ans+=dfs(i);
	}
	cout<<n-ans<<'\n';
}

CF1439B Graph Subset Problem

给定 \(n\) 个点 \(m\) 条边的图,你需要输出一个 \(k\) 个点的团,或者一个 \(k-core\)\(k-core\) 的含义是,一个点集,它的导出子图的任意一个点的度数都 \(\ge k\)
\(n,m,k\le 10^5\)

看到团肯定想根号,团的大小是不可能超过 \(\sqrt{2m}+1\)
同时,不论是 \(k\) 个点的团还是 \(k-core\),每个点的度数都 \(\ge k-1\),所以先不断删 \(deg<k-1\) 的点直到不存在。
如果我们打算输出的子集包含 \(deg=k-1\) 的点,那肯定是团了,就枚举每个 \(deg=k-1\) 的点 \(O(k^2)\) 判断其 \(k-1\) 个邻居是否能与它构成团(用unordered_map存邻接矩阵)。然后将这个点删去。
如果还剩下点,那它们肯定能构成 \(k-core\)

posted @ 2023-03-13 17:48  pengyule  阅读(38)  评论(0编辑  收藏  举报