大枝江、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\)。