牛客寒假算法训练营1-D

并查集、求逆元、排列组合

题目传送门

解题思路

首先要求出没有修改时的方案数量,再考虑修改一个格子之后相对于之前的变化

1 求没有修改时方案数量

首先使用并查集操作求出连通块的个数每个连通块的大小

假设有\(cnt\)个连通块,然后每个连通块的大小是\(size_i , i\in [1, cnt]\)

那么根据题意,利用排列组合的简单知识可以知道,总的方案数为:\(ans = cnt!*\prod_{i=1}^{cnt}size_i\)

2 修改了一个格子相对于之前的变化

将一个各自的数字变成1有两种情况

  1. 这个格子原本就是1,那么直接输出上次的ans即可
  2. 这个格子原本是0

对于第二种情况,若\(a[x][y] == '0'\) , 我们可以这样理解:

step1:

首先我们把它变成\('1'\),把他当成一个独立的连通块,则此时\(cnt = cnt + 1; ans = ans*cnt\)

step2:

然后看坐标\((x,y)\)的上下左右四个方向是不是\('1'\),如果是的话,就将这个\((x,y)\)所在的连通块和该方向的点所在的联通块合并。

例如: \(a[x-1][y]=='1'\), 那么 假设 \(a[x][y]\)所在连通块的根是\(f_1\),大小是\(size_1\)\(a[x-1][y]\)所在连通块的根是\(f_2\),大小是\(size_2\),这两个连通块可以合并,合并之后方案数为:\(ans = (ans/cnt/size_1/size_2)*(size_1 + size_2)\)


知识点:逆元

有两种求逆元的方法:扩展欧里几德算法求逆元、费马小定理求逆元

费马小定理求逆元德方法比较简单,但是适用范围比较小

p是质数,且a、p互质,则: \((a/b)\%p = (a*a^{p-2})\%p\),其中\(a^{p-2}\)使用快速幂算法求


ac代码

#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int MaxN = 510;
const int mod = 1e9+7;
int n, k, x, y;
char a[MaxN][MaxN];
int fa[MaxN*MaxN]; // 
int size[MaxN*MaxN];
int cnt = 0; // 连通块的个数 
ll ans = 1;
int dx[] = {0, 0, -1, 1};
int dy[] = {-1, 1, 0, 0};
// 并查集的并、查操作
int find(int x)
{
	return fa[x] == x ? x : fa[x] = find(fa[x]);
 } 
void unionn(int x, int y)
{
	int fax = find(x);
	int fay = find(y);
	if(fax !=  fay){
		size[fax] += size[fay];
		fa[fay] = fax;
	}
}
ll ksm(int n , int k)
{
	ll ans = 1;
	ll tot = n;
	while(k){
		if(k&1) ans = ans*tot%mod;
		tot = tot*tot%mod;
		k >>= 1;
	}
	return ans%mod;
}
// 求逆元
inline ll inv(int x)
{
	return ksm(x, mod-2);
}
int main()
{
	scanf("%d", &n);
	for(int i=1; i<=n; ++i){
		scanf(" %s", a[i]+1);  // 左上角的坐标从(1,1)开始计算
	}

//	printf("*********\n");
//	for(int i=1; i<=n; ++i){
//		for(int j=1; j<=n; ++j){
//			printf("%c", a[i][j]);
//		}
//		printf("\n");
//	}
//	printf("**********\n");
	// 初始化 
	for(int i=0; i<=(n+1)*(n+1); ++i){ // 一定注意这里是(N+1)*(n+1), 也可以直接在上面n++
		fa[i] = i;
		size[i] = 1;
	}
	// 合并连通块 
	for(int i=1; i<=n; ++i){
		for(int j=1; j<=n; ++j){
			if(a[i][j]=='1'){  // 看上下左右有无‘1’ 有的话合并 
				if(a[i-1][j]=='1') unionn(i*n+j, (i-1)*n+j);
				if(a[i+1][j]=='1') unionn(i*n+j, (i+1)*n+j);
				if(a[i][j-1]=='1') unionn(i*n+j, i*n+j-1);
				if(a[i][j+1]=='1') unionn(i*n+j, i*n+j+1);
			}
		}
	}
	// 求连通块的个数 // 计算初始解
	for(int i=1; i<=n; ++i){
		for(int j=1; j<=n; ++j){
			if(a[i][j]=='1' && fa[i*n+j] == i*n+j) {
				cnt++;
				ans = (ans * size[fa[i*n+j]])%mod;	
			}
		}
	} 
	for(int i=1; i<=cnt; ++i){
		ans = (ans*i)%mod;
	}
	
	scanf("%d", &k);
	while(k--){
		scanf("%d %d",&x, &y);
		x++, y++;
		if(a[x][y]=='1'){
			printf("%lld\n",ans);
			continue;	
		}
		a[x][y] = '1';
//		fa[x*n+y] = fa[x*n+y]; // 这个多余,可以去掉 
		// 先将这个'1'当成一个单独的连通块,计算ans 
		cnt++;
		ans = (ans*cnt)%mod;
		for(int i=0; i<4; ++i){
			int xx = x+dx[i];
			int yy = y+dy[i];
			if(a[xx][yy]=='1'){
				int f1 = find(x*n+y);
				int f2 = find(xx*n+yy);
				if(f1!=f2){ // 两个连通块合并 
					// ans = ans/cnt/size[f1]/size[f2]*(size[f1]+size[f2])%mod
					ans = (ans*inv(cnt))%mod;
					ans = (ans*inv(size[f1]))%mod;
					ans = (ans*inv(size[f2]))%mod;
					ans = (ans*(size[f1]+size[f2]))%mod;
					unionn(f1, f2); // 合并连通块 
					cnt--; // 连通块数量-1 
					
				}
				 
			}
		}
		printf("%lld\n",ans);
	}
	return 0;
}
 

参考链接

https://ac.nowcoder.com/acm/contest/view-submission?submissionId=46654023

https://www.cnblogs.com/kongbursi-2292702937/p/10582258.html

https://www.cnblogs.com/thornblog/p/11889737.html

posted @ 2021-02-28 17:21  VanHope  阅读(73)  评论(0编辑  收藏  举报