CF1545C AquaMoon and Permutations 题解

本文详细解析了一种解决给定特定条件的拉丁方阵构造问题的算法。通过分析图的完美匹配性质,提出了一种递归策略,确保每次选择一个合适的排列并删除相关连接,最终得出解决方案。算法复杂度为O(n^3),适用于题目所给的规模。代码实现清晰,逻辑严谨。

Description

给定一个 2n2n2n 个长度为 nnn 的排列,你需要从中选出 nnn 个组成一个拉丁方阵。

保证: 若对于每两个存在对应位置的值相同的排列连边,则此图有完美匹配。

1≤n≤5001 \le n \le 5001n500

Solution

我们刚拿到这个方阵,第一步该如何处理呢?

首先,如果某一列上,有一个数出现了恰好一次,那么该数所在的排列必然出现在最终的拉丁方阵里面。于是,选定它,然后将所有与它有连边的排列删掉。

其次,如果所有数的出现次数都超过一次呢?不难发现,根据抽屉原理(鸽巢原理),每个数的出现次数恰好为两次。这意味着什么呢?对于每一个方阵,包含它的拉丁方阵数量恰好等于不包含它的拉丁方阵数量。因此,我们可以随意从中挑出一个排列,删去与其有边直接相连的排列。注意也要将计数的答案乘 222

现在,我们得到了每一列上有 n−1n-1n1 个数的子问题。我们似乎可以直接递归处理。然而,考虑这样的一种情况: 在上述的第一步中,我们删去了一个排列,并发现它没有出边。那么,在这一轮操作中,遗留的方阵有 2n−12n-12n1 个,如果我们在某一列上找不到一个数出现恰好一次,那么就不一定所有数的出现次数均为 222,可能有某个数的出现次数为 333。这样,上述解法就无力回天了。

所以我们该怎么办呢?着眼于题目给出的奇怪限制: 图有完美匹配。这意味着什么呢?意味着,在第一轮操作中,选定的点一定有出边!遗留的排列数量不会超过 2n−22n-22n2!于是,上面的情况不会出现,第二轮依然可以这么处理!

第三轮、第四轮 ⋯⋯\cdots \cdots 我们是不是都能这么干?我们果断猜想是可以的,但是我们需要一个强有力的证明。不难发现,我们要证明的东西就是: 对于一张 nnn 个点的,有完美匹配的图,每次在图上任意选出一个点,并将所有与它相邻的点删去,那么无论何时,nnn 减去已经结束的轮数的两倍不会超过剩余的点数。

注意到,对于任意 KKK,第 1,2,3⋯ ,K1,2,3\cdots,K1,2,3,K 轮中删掉的总点数不小于 2K2K2K。于是,上面的定理正确。

综上所述,我们每次判断是否有一列上存在某个数出现次数恰好为 111 次。如果存在的话,找到这个排列,将所有与它相邻的点以及它自己删去;如果不存在的话,将第一个答案乘 222,并随意钦定一个在最终拉丁方阵中的排列,将其与其相邻点删去。递归处理即可。

总复杂度 O(n3)O(n^3)O(n3),可以通过本题。

感觉讲的很清楚明白 ,当然可能是我太自恋了

Code

#include <bits/stdc++.h>
using namespace std;
const int maxl=1005,mod=998244353;

int read(){
	int s=0,w=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-')  w=-w;ch=getchar();}
	while (ch>='0'&&ch<='9'){s=(s<<1)+(s<<3)+(ch^'0');ch=getchar();}
	return s*w;
}
int t,n,len,ans=1;
int a[maxl][maxl],used[maxl],ans2[maxl],tmp[maxl],pos[maxl];

void Choose(int now){
	used[now]=1,ans2[++len]=now;
	for (int i=1;i<=2*n;i++){
		if (i==now||used[i])  continue;
		
		int flag=0;
		for (int j=1;j<=n;j++){
			if (a[i][j]==a[now][j]){
				flag=1;
				break;
			}
		}
		if (flag)  used[i]=1;
	}
}

void solve(){
	int cur=0;
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++)  tmp[j]=0,pos[j]=0;
		for (int j=1;j<=2*n;j++){
			if (used[j])  continue;
			tmp[a[j][i]]++,pos[a[j][i]]=j;
		}
		for (int j=1;j<=n;j++){
			if (tmp[j]==1){
				cur=pos[j];
				break;
			}
		}
		if (cur)  break;
	}
	if (cur)  Choose(cur);
	else{
		ans=(ans*2)%mod;
		for (int i=1;i<=2*n;i++){
			if (!used[i]){
				Choose(i);
				break;
			}
		}
	}
}

void clear(){
	ans=1,len=0;
	memset(used,0,sizeof(used));
	memset(pos,0,sizeof(pos));
	memset(tmp,0,sizeof(tmp));
}

signed main(){
	t=read();
	while (t--){
		n=read();
		for (int i=1;i<=2*n;i++){
			for (int j=1;j<=n;j++)  a[i][j]=read();
		}
		for (int i=1;i<=n;i++)  solve();
		
		printf("%d\n",ans);
		sort(ans2+1,ans2+n+1);
		for (int i=1;i<=n;i++)  printf("%d ",ans2[i]);
		clear(),puts("");
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值