CF1916F-Group Division

CF1916F-Group Division

题目大意

给定一个\(n\)个点的无向连通图G,保证连通图没有割点。给定两个正整数\(n_1,n_2\),要求把连通图划分为两块连通图,大小分别为\(n_1,n_2\),保证\(n_1+n_2=n\)。题目保证有解。

做题思路

事先说明:本人系改良主义,做题思路就是做,错,改,错,改,一直到对为止,下文会有很多“真的吗”“错了怎么办”的自问自答环节,而且有很多自然语言未做规范定义,受不了就别看
我的突破口在于,我觉得对于任意无割点连通图而言,\(\forall n_1,n_2 \in [0,n]\),只要满足\(n_1+n_2=n\),都是有解的,也就是\(n_1,n_2\)怎么取都有解。这样的想法好处在于不用考虑\(n_1,n_2\)的影响。刻意去划分出\(n_1,n_2\)的大小属实难受。
然后,我就把题目就变成了,把图上的点排成一个序列,任何不是序列头的点,都和其前面所有点至少有一条连边,任何不是序列尾的点,都和其后面所有点至少有一条连边。有了这么一个序列,序列所有前缀与后缀都是连通图,这样任意大小的划分都可以做到了。
有了方向,开始尝试做题。首先把1送入序列,把所有与1相连的点放入一个队列中,我称呼这个队列为激发队列,把与某一个点相连的所有点送入激发序列,称为激发。每次从队列中取出一个点,送入序列,再激发所有直接相连的点,以此类推直到所有点都入队。
不过这样有一些问题。

  1. 要是有的点还没入队,激发队列先空了怎么办?
    答:不可能,因为是连通图。
  2. 怎么保证序列的性质?
    答:做法其实有问题。首先这么做肯定能保证与前面的点相连,但是不能保证与后面的点相连。要是一个点入队时所有直接相连的点都已经入队就保证不了性质了。

做法有问题,想办法改良。
问:为什么会出现性质错误情况?或者说,性质错误是是什么状态?
答:这种情况下,某一个点与后面的点没有相连,可以认为在只包含这个点和这个点后面的点的子图中,这个点与其后缀点不连通。也就是被割裂了。那么,其前面的所有点就构成了一组割点集(应该是这个概念吧),这部分点把剩余的图割裂为了不止一部分。

已经知道错误原因,那么只要保证不存在割裂 余图 (其他所有没入队的点构成的子图) 的状况,就不会出现性质错误的情况。在一个点入队之前,先判断是否会会割裂余图,如果会,就记为“割点”,处理其他的点。直到除了割点以外没有其他点被激发。

现在是有一块连通图G1,连着很多个点,集合记为S1,G1和任意S1中的点V都会使子图(G-G1-V)不是连通图,记G2=(G-G1-s1)
在图G2+S1上,有一些结论

  1. G1与G2不连通
    证明:若是相连,G1就会继续扩张,G2的点要么被判割点入S1,要么入G1.但是扩张已经停止,所以不连通。

  2. G2+S1范围内,任意V属于G2,S1中有至少两个点可以到达V。
    证明:反证法。若存在V不能到达任意S1的点,V属于G2不能到达G1,那么图G不连通,矛盾
    若存在V只能到达s1中的一个点,那么这个点就是图G的割点,也矛盾。
    所以,v至少能到达两个点。

  3. S1+G2是连通图
    显然,因为G1扩张始终不会加入割点,也就是G1不会割裂G-G1,而G-G1=G2+S1.G2+S1是连通图。
    特殊说明,在任意时刻,G-G1是连通图都成立,性质一则是G1扩张完毕最后才成立

  4. 先定义:如果存在一条路径,起点和终点是两个属于S1的不同点,而路径中除了起点和终点的所有点都是属于G2的点,那么称起点和终点可到达。
    性质:S1中的点至少能到达S1中除自己外的至少一个点。
    证明:设点V,因为G2+S1是连通图,点V与S1+G2-V相连,若V与S1-V相连就直接得证,若V与G2的某个点相连,记为U,在图G2+S1中,这个点可以到达S1的至少两个点,V可以先到达U,再到达U可到达的另外点。

  5. 把S1中两点的可到达关系作为边,做一个新图G3,G3无环。
    反证法:假设有环,尝试任意删去一个S1中一个点V,首先G2中的任意一点可以到达S1中的至少两个点,删去一个点,G2的所有点仍然可到达S1中的其中一点,若是G3有环,删去一个点仍然连通,也就是通过G2,S1也是连通的,而G2所有点可到达S1.G2+S1-V是连通图。

    如果G2+S1-V是连通图会发生什么呢?我们称V被判割点时刻的G1为G1(V),显然G1(V)是G1的子图,若G1=G1(v),场上只剩下S1+G2-V,不成割点,错误。如果G1-G1(v)不空,为了V被判割点,只能G-G1(V)与 S1+G2-V不连通,但是由于性质3,G-G1(V)得与S1+G2连通,所以V与G-G1(V)连通。

    总结前两段就是,为了使S1的任意点V,有V+G1(V)割裂余图,同时要求G3有环,必须G1-G1(V)非空,且G1-G1(V)与V连通,但是假设有一点U,在V之后才进入S1,那么G-G1(U)是G-G1(V)的子图,也就是U与G-G1(V)相连,与G-G1(V)和S1-V不连通相矛盾。也就是说S1有两个以上的点可以证明G3无环,而S1只有一个点的话也成不了环。得证。

由性质5可知,G3一定有点度为1,毕竟若每个点有两条以上连边一定成环。

推了这么多性质,回归做法,我的做法就是,找到度数为1的点,(度数为1的点此时一定不是割点,大家考虑S1,G2,G3的关系自己证明吧,我懒的证明了)入队激发,直到再次没有点可以激发为止,但是每次进入这种状态G1都在扩大,所以最后一定会做完。

复杂度最多每个点均摊入队一次,判割点一次,找G3度为1的点一次,均摊\(o(n+m)\),总复杂度\(o(n*(n+m))\)
最后贴上我丑陋的代码

点击查看代码
#include<bits/stdc++.h> 
#define ll long long 
//#define int long long
#define mid ((l+r)>>1)
#define lson u<<1,l,mid
#define rson u<<1|1,mid+1,r
#define inf 0x3f3f3f3f 
#define pii std::pair<int,int>
typedef int LL;
const signed maxn=(signed)3e3+5;
const signed maxm=(signed)6e3+5;
inline LL read(){
	char ch=getchar();bool f=0;LL x=0;
	for(;!isdigit(ch);ch=getchar())if(ch=='-')f=1;
	for(;isdigit(ch);ch=getchar())x=(x<<1)+(x<<3)+(ch^48);
	if(f==1)x=-x;return x;
}
struct Edge{
	int to,nxt;
}edge[maxm<<1];
int head[maxn],ecnt;
void adde(int u,int v){
	edge[++ecnt]=(Edge){v,head[u]};
	head[u]=ecnt;
}
int n1,n2,n,m;
bool inque[maxn],mot[maxn];
std::queue<int> mque;
void motivate(int u){
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(!inque[v]&&!mot[v]) mot[v]=1,mque.push(v);
	}
}
int link[maxn];
bool is_cut(int x){
	for(int i=1;i<=n;++i) link[i]=inque[i];
	std::queue<int> q;
	while(!q.empty()) q.pop();
	link[x]=1;
	for(int i=1;i<=n;++i){
		if(!link[i]){
			link[i]=1;q.push(i);break;
		}
	}
	while(!q.empty()){
		int u=q.front();q.pop();
		for(int i=head[u];i;i=edge[i].nxt){
			int v=edge[i].to;
			if(!link[v]){
				link[v]=1;q.push(v);
			}
		}
	}
	for(int i=1;i<=n;++i) if(!link[i]) return true;
	return false;
}
int cut[maxn],ccnt;
int arr[maxn],acnt;
void in_que(int u){
	inque[u]=1;arr[++acnt]=u;motivate(u);
}
 int tp[maxn],tpcnt;
bool becut[maxn];
int spread(std::queue<int>& upp,int x){
	std::queue<int> q;
	q.push(x);int ans=0;
	while(!q.empty()){
		int u=q.front();q.pop();
		for(int i=head[u];i;i=edge[i].nxt){
			int v=edge[i].to;
			if(becut[v]&&!link[v]) ++ans,upp.push(v),link[v]=1; 
			else if(!link[v]){
				link[v]=1;q.push(v);
			}
		}
	}
	return ans;
}
void solve_cut(){
	for(int i=1;i<=n;++i) link[i]=inque[i];
	std::queue<int> q;
	for(int i=1;i<=n;++i){
		if(becut[i]){
			q.push(i);break;
		}
	}
	int res=0;
	while(!q.empty()){
		int u=q.front();q.pop();
		link[u]=1;
		int call=spread(q,u)+res;
		//printf("%d call=%d\n",u,call);
		if(call<2) in_que(u),becut[u]=0,--ccnt;
		res=1;
	}
}
signed main()
	{	 
		LL T=read();
		while (T--){
			n1=read(),n2=read(),m=read();
			n=n1+n2;
			for(int i=1;i<=n;++i) head[i]=0;
			ecnt=0;acnt=0;ccnt=0;
			for(int i=1;i<=m;++i){
				int u(read()),v(read());
				adde(u,v),adde(v,u);
			}
			for(int i=1;i<=n;++i) inque[i]=0;
			for(int i=1;i<=n;++i) mot[i]=0;
			while(!mque.empty()) mque.pop();
			mque.push(1);
			while(acnt!=n){
				if(mque.empty()){
					solve_cut();
					continue;
				}
				int u=mque.front();mque.pop();
				if(inque[u]) continue;
				//if(is_cut(u)) cut[++ccnt]=u;
				if(is_cut(u)) becut[u]=1,++ccnt;
				else in_que(u);
			}
			//printf("n1=%d,n2=%d\n",n1,n2);
			for(int i=1;i<=n1;++i) printf("%d ",arr[i]);putchar('\n');
			for(int i=1;i<=n2;++i) printf("%d ",arr[i+n1]);putchar('\n');
		}
		return 0;
	}	
posted @ 2024-03-01 17:27  LOOP_0  阅读(37)  评论(0)    收藏  举报