开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情
题目链接
题目
题目大意
一个 的网格中每个网格里可以放一个时钟,的合法显示为 ,每个时钟总是精确地显示某个小时(也就是说,它不在两个小时之间)。我们定义一次操作为:
- 选择某一行或某一列,并将该行或列中的所有时钟向前移动一小时。(如果时钟当前显示 ,向前移动一小时后该时钟将显示 。
如果可以通过进行若干次操作,使所有时钟显示相同的时间,则时钟网格称为可解。
在最初网格中的一些格子里已经存在一些时钟已经显示了某个初始时间,而其余单元格是空的。
给定部分完成的时钟网格,找出在空单元格中分配时钟的方法,以便网格可解。答案可能是巨大的,所以计算它对 取余的结果。
如果存在时钟在两种排列中显示不同时间的某个单元,则两种分配不同。
思路
如果输入的两个时钟在同一列,且分别显示 和 ,则说明最终时钟 所在行比时钟 所在行被操作次数多 。
我们可以以行为节点建立加权并查集,两个行如果存在在同一列的时钟就将其合并进同一个集合,边权为显示时间之差对 取余的结果。若在合并过程中遇到冲突,则不存在解决方案。
合并结束后,我们此时把行分成了 个集合。对于两个不同的集合 和 ,我们可以任意差值连接它们:若 中所有行的第 列都不存在元素,而 T 中某行的第 列存在元素,我们只需要在 中的任意一行第 列随意进行填充即可。在 中的任意一行第 列随意进行填充的方案数为 ,所以把所有集合全部合并为一个集合的方案数是 。
此时,如果一列里存在一个数字,因为行和行之间的差值已经确定,所以每一行的该列应该填充的数字也已经确定。如果一列里不存在任何时钟,我们可以随便在该列放一个时钟,则行时钟的示数也已经确定。令 表示空列的数量,则填上空列的方案是
代码
#include <iostream>
#include <algorithm>
#include <math.h>
#include <stdio.h>
#include <map>
#include <vector>
#include <string.h>
#include <queue>
#define nnn printf("No\n")
#define yyy printf("Yes\n")
using namespace std;
using LL=long long;
const int N=500001;
const LL mod=1000000007;
//const LL mod=998244353;
int f[N],d[N];
int n,m,k,x,y,z,tot;
int cid[N],cval[N];
LL h;
int find(int x)
{
if (x==f[x]) return x;
find(f[x]);
d[x]=(d[x]+d[f[x]])%h;
f[x]=f[f[x]];
return f[x];
}
LL poww(LL a,int b)
{
LL ans=1;
for (;b;b>>=1,a=a*a%mod)
if (b&1) ans=ans*a%mod;
return ans;
}
LL solve()
{
scanf("%d%d%lld",&n,&m,&h);
for (int i=1;i<=n;++i) f[i]=i,d[i]=0;
for (int i=1;i<=m;++i) cid[i]=cval[i]=0;
int flag=1;
for (int i=1;i<=n;++i)
for (int j=1;j<=m;++j)
{
scanf("%d",&x);
if (x==-1) continue;
if (cid[j]==0) cid[j]=i,cval[j]=x;
else
{
if (find(cid[j])!=find(i))
{
d[f[i]]=((cval[j]+h-x)%h+h-d[i])%h;
f[f[i]]=cid[j];
}
else
{
if (d[i]!=(d[cid[j]]+(cval[j]+h-x)%h)%h) flag=0;
}
}
}
if (!flag) return printf("0\n"),0;
int cnt=0;
for (int i=1;i<=n;++i)
if (f[i]==i) cnt++;
for (int i=1;i<=m;++i) cnt+=(cid[i]==0);
printf("%lld\n",poww(h,cnt-1));
}
int main()
{
int T=1;
scanf("%d",&T);
while (T--) solve();
return 0;
}