2019牛客暑期多校maximum clique 1 求最大独立团点集

maximum clique 1

先上题目连接

题目描述

You are given a set S S S containing n distinct positive integers a 1 , a 2 , . . . , a n . a_1,a_2,...,a_n. a1,a2,...,an.

Please find a subset of S S S which has the maximum size while satisfying the following constraint:

The binary representations of any two numbers in this subset must have at least two different bits.

If there are multiple valid subsets, please output any one of them.
题目输入输出描述在这里插入图片描述

题意

给出一个集合,找到这个集合中最大的子集,子集满足的条件为任意两个数的二进制表示至少有两位不相同。输出该子集的元素个数以及各个元素。

分析

按照题目描述,我们给至少两位不相同的数建边,求这个图的最大团。
最大团等于其补图的最小点覆盖。
补图为只有一位不同的两个数建边,分析可知这是一个二分图——按照含有位数1个数奇数和偶数划分成两个点集,两个点集之间肯定不存在一条边(只有一位不同的两个数才能建边),所以这是一个二分图。
而二分图的最小点覆盖等于点数个数减去最大匹配数。
所以这道题转化为用匈牙利算法求最大匹配数和最小点覆盖。
补充一下图论的基础概念。

图论基础概念

最大匹配

 “任意两条边都没有公共端点”的边的集合被称为图的一组匹配。在二分图中,包含边数最多的一组匹配被称为二分图的最大匹配

最小点覆盖

 二分图中最小的点集 S S S使得图中任意一条边都至少有一个端点属于 S S S
Konig定理
 二分图最小点覆盖包含的点数等于二分图最大匹配包含的边数。

最大独立集

 “任意两点之间都没有边相连”的点集称为独立集。包含点数最多的一个就是图的最大独立集

最大团

 “任意两点之间都有边相连”的子图被称为无向图的“”。点数最多的团称为图的最大团
定理

  • 无向图 G G G最大团等于其补图 G ′ G' G最大独立集
  • G G G是有 n n n个结点的二分图, G G G最大独立集 = n − =n- =n最大匹配数

题解代码

#include <stdio.h>
#include <cstring>
using namespace std;
const int MAXN=5e3+5;
int a[MAXN],n,uvis[MAXN],vvis[MAXN],ulink[MAXN],vlink[MAXN],col[MAXN];
int head[MAXN],cnt,vis[MAXN];
struct Edge{int next,to;};
Edge e[MAXN*MAXN+2];//这里注意我的建边方法是n^2条边,n为点数
void Add(int u,int v){e[++cnt].next=head[u];e[cnt].to=v;head[u]=cnt;}
int lowbit(int x){return (x&(-x));}
bool FindPath(int u){
	uvis[u]=1;
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to;
		if(vvis[v])continue;
		vvis[v]=1;
		if(!vlink[v]||FindPath(vlink[v])){
			vlink[v]=u;ulink[u]=v;
			return true;
		}
	}
	return false;
}

void color(int u){//染色,这道题也可以不染色
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to;
		if(vis[v])continue;
		vis[v]=1;col[v]=col[u]^1;
		color(v);
	}
}
void hungary(){
	int ans=0;
	for(int i=1;i<=n;++i){
		if(col[i]) continue;
		memset(vvis,0,sizeof(vvis));
		ans+=FindPath(i);
	}
	memset(vvis,0,sizeof(vvis));
	memset(uvis,0,sizeof(uvis));
	printf("%d\n",n-ans);
	for(int i=1;i<=n;++i)if(!col[i]&&!ulink[i])FindPath(i);
	for(int i=1;i<=n;++i)if((!col[i]&&uvis[i])||(col[i]&&!vvis[i])) printf("%d ",a[i]);
	puts("");
}
int main(){
	memset(col,-1,sizeof(col));memset(vis,0,sizeof(vis));
	scanf("%d",&n);
	for(int i=1;i<=n;++i)scanf("%d",&a[i]);
	for(int i=1;i<=n;++i)
	for(int j=1;j<i;++j)if(lowbit(a[i]^a[j])==(a[i]^a[j])){Add(i,j);Add(j,i);}
	for(int i=1;i<=n;++i)
	for(int i=1;i<=n;++i)if(col[i]==-1){col[i]=0;color(i);}
	hungary();
	return 0;
}

和大佬们学习的做法,参照了许多大佬的代码。

建议对匈牙利算法理解不深刻的同学先做一下下面两个模板题。我重新学习了一下匈牙利算法,发现自己之前理解的并不到位。
先上两个模板题。

HihoCoder - 1122 二分图最大匹配之匈牙利算法

#include<cstdio>
#include <algorithm>
#include <cstring>
using namespace std;

int n, m, u, v;
const int MAXN = 1e4+5;
int found[MAXN];
bool vis[MAXN], M[MAXN][MAXN];

bool FindPath(int u){
	for(int i=1; i<=n; ++i){
		if(M[u][i]&&!vis[i]){
			vis[i]=1;
			if(found[i]==0||FindPath(found[i])){
				found[i]=u;
				found[u]=i;
				return 1;
			}
		}
	}
	return 0;
} 

int main(){
	scanf("%d%d",&n,&m);
	memset(M,0,sizeof(M));
	memset(found,0,sizeof(found));
	for(int i=0;i<m;++i){
		scanf("%d%d",&u,&v);
		M[u][v]=M[v][u]=1; // 无向图双向建边
	}
	int ans=0;
	for(int i=1;i<=n;++i){
		if(found[i])continue; //如果没有匹配过才寻找
		memset(vis,0,sizeof(vis));
		ans+=FindPath(i);
	}
	printf("%d\n",ans);
	return 0;
}

POJ-3041 Asteroids

#include <algorithm>
#include <stdio.h>
#include <cstring>
using namespace std;
const int MAXN = 1e5+6;
int head[MAXN],cnt=0;
struct edge{int next,to;};edge e[2*MAXN];
void add(int u, int v){e[++cnt].next = head[u];e[cnt].to = v;head[u] = cnt;}
bool vis[MAXN];
int n,m,u,v,found[MAXN];

bool FindPath(int u){
	for(int j = head[u]; j; j = e[j].next){
		int v = e[j].v;
		if(vis[v])continue;
		vis[v] = 1; // vis
		if(!found[v] || FindPath(found[v])){
			found[v] = u; // numy记录其中一个集合的匹配者 
			return true;
		}
	}
	return false;
}

int maxmatch(){
	int ans = 0;
	for(int i = 1; i <= n; i++){
		memset(vis,0,sizeof(vis));//全部跑一遍
		ans+=FindPath(i);
	}
	return ans;
}

int main(){
	scanf("%d%d",&n,&m);
	for(int i = 0; i < m; i++){
		scanf("%d%d",&u,&v);
		add(u,v); //单向边
	}
	printf("%d",maxmatch());	
	return 0;
}

对比代码,我们发现,同样是匈牙利算法。
下面POJ的题只需建单向边,把所有的点都跑一遍。只需要记录单项匹配。

if(!found[v] || FindPath(found[v])){
	found[v] = u; // numy记录其中一个集合的匹配者 
	return true;
}

而上面hiho的题要建双向边,只需要跑还未匹配过的顶点。需要记录双向匹配。

if(found[i]==0||FindPath(found[i])){
	found[i]=u;
	found[u]=i;
	return 1;
}

这是因为POJ的题行和列为两个不同的点集, u → v u \rightarrow v uv v → u v \rightarrow u vu是不一样的,代表两个不同的点的坐标,所以是有向图。并且是两个集合(确定好的二分图不同集合)
而hiho的题目给出的是点集,为无向图,二分图的两个点集并不确定,所以已经匹配过后的点不需要再次搜索一遍,为未匹配过的点进行匹配就可以了。

回过头来看牛客的这道题目:
建图的话我选择判断:两个数异或和的lowbit等于这两个数异或和,为这两个数的编号建双向边(相当于每个点有其点权的无向图)。
最大匹配数很好求,这里就不解释了。
我们要求出最大独立集的点集。其实本质上就相当于染色的过程。

我首先跑了个染色,分出二分图的两个点集(这个题意下也可以不跑,直接判断两个点的二进制1的位数和是奇数还是偶数即可)
我们把染过色的两个点集称为 A A A B B B.

void color(int u){//染色,这道题也可以不染色
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to;
		if(vis[v])continue;
		vis[v]=1;col[v]=col[u]^1;
		color(v);
	}
}

寻找增广路的算法中,我定义了 u v i s uvis uvis v v i s vvis vvis两个访问数组,分别用来标记以 u u u为起点的路径和以 v v v为终点的路径。
定义了 x l i n k xlink xlink y l i n k ylink ylink两个数组分别记录以 u u u为起点的匹配者和以 v v v为终点的匹配者。

bool FindPath(int u){
	uvis[u]=1;
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to;
		if(vvis[v])continue;
		vvis[v]=1;
		if(!vlink[v]||FindPath(vlink[v])){
			vlink[v]=u;ulink[u]=v;
			return true;
		}
	}
	return false;
}

在匈牙利函数中,先跑一遍最大匹配数,类似于hiho,只跑其中一个集合 A A A的点就可以,每次都清空vvis数组就好。
下面再次对一个集合 A A A跑增广路算法,如果 A A A集合的点被跑过或者 B B B集合中的点没被跑过,都是最大独立集中的点,直接输出即可。

void hungary(){
	int ans=0;
	for(int i=1;i<=n;++i){
		if(col[i]) continue;
		memset(vvis,0,sizeof(vvis));
		ans+=FindPath(i);
	}
	memset(vvis,0,sizeof(vvis));
	memset(uvis,0,sizeof(uvis));
	printf("%d\n",n-ans);
	for(int i=1;i<=n;++i)if(!col[i]&&!ulink[i])FindPath(i);
	for(int i=1;i<=n;++i)if((!col[i]&&uvis[i])||(col[i]&&!vvis[i])) printf("%d ",a[i]);
	puts("");
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值