牛客寒假算法训练营1-D
并查集、求逆元、排列组合
解题思路
首先要求出没有修改时的方案数量,再考虑修改一个格子之后相对于之前的变化
1 求没有修改时方案数量
首先使用并查集操作求出连通块的个数和每个连通块的大小
假设有\(cnt\)个连通块,然后每个连通块的大小是\(size_i , i\in [1, cnt]\)
那么根据题意,利用排列组合的简单知识可以知道,总的方案数为:\(ans = cnt!*\prod_{i=1}^{cnt}size_i\)
2 修改了一个格子相对于之前的变化
将一个各自的数字变成1有两种情况
- 这个格子原本就是1,那么直接输出上次的ans即可
- 这个格子原本是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