题目解析
本题的核心是通过已知的奇数位置点 p 1 , p 3 , . . . , p 2 k + 1 p1, p3, ..., p2k+1 p1,p3,...,p2k+1,恢复一条完整的简单路径 p 1 , p 2 , . . . , p 2 k + 1 p1, p2, ..., p2k+1 p1,p2,...,p2k+1。路径需要满足以下条件:
- 每个相邻点 p i pi pi 和 p i + 1 pi+1 pi+1 必须共享一条边。
- 路径不能自交(即路径上的点互不相同)。
我们需要计算所有可能的路径补全方案数,并对结果取模 1 0 9 + 7 10^9 + 7 109+7。
建模思路
核心建模
- 奇数点和偶数点的关系:每一对奇数点 p 2 i − 1 p2i-1 p2i−1 和 p 2 i + 1 p2i+1 p2i+1 之间必须插入一个中间点 p 2 i p2i p2i,且 p 2 i p2i p2i 必须与 p 2 i − 1 p2i-1 p2i−1 和 p 2 i + 1 p2i+1 p2i+1 相邻。
- 候选点的选择:
- 如果 p 2 i − 1 p2i-1 p2i−1 和 p 2 i + 1 p2i+1 p2i+1 在同一行或列上相距 2,则有唯一的一个中点作为候选。
- 如果 p 2 i − 1 p2i-1 p2i−1 和 p 2 i + 1 p2i+1 p2i+1 呈“对角”关系(行列各差 1),则有两个候选点。
- 图模型:将所有候选点视为图中的节点,每一段 ( p 2 i − 1 , p 2 i + 1 ) (p2i-1, p2i+1) (p2i−1,p2i+1) 的候选点之间连一条边(如果候选点相同,则形成自环)。问题转化为在这张图中选择每个边的一个端点,使得不同边选择的端点互不相同。
图的性质
- 节点数:最多为 n × m n × m n×m。
- 边数:最多为 k + ( k + 1 ) k + (k+1) k+(k+1)。
- 连通分量:每个连通分量要么是一棵树,要么包含一个环。
并查集统计法
我们使用并查集(DSU)来维护图的连通分量,并记录每个分量的以下信息:
- 节点数 s [ i ] s[i] s[i]:分量中不同候选点的数量。
- 边数 e [ i ] e[i] e[i]:分量中段边(包括自环)的总数。
- 预占用标记 l o o p [ i ] loop[i] loop[i]:如果某个奇数点正好出现在这个分量中,则标记为“已预占用”。
计算答案的四种情况
1. e [ i ] > s [ i ] e[i] > s[i] e[i]>s[i]
解释:
如果边数
e
[
i
]
e[i]
e[i] 大于节点数
s
[
i
]
s[i]
s[i],说明该分量中存在冲突(某些边无法独立选择端点)。因此,这种情况下的方案数为 0。
示例:
假设分量中有两个点
A
,
B
A, B
A,B,但有三条边
(
A
−
B
)
,
(
A
−
B
)
,
(
A
−
B
)
(A-B), (A-B), (A-B)
(A−B),(A−B),(A−B)。无论如何选择端点,都会导致重复选点的情况。因此,方案数为 0。
2. e [ i ] < s [ i ] e[i] < s[i] e[i]<s[i] (即树结构)
解释:
如果边数
e
[
i
]
e[i]
e[i] 小于节点数
s
[
i
]
s[i]
s[i],说明这是一个树结构。对于树结构,方案数等于节点数
s
[
i
]
s[i]
s[i]。
示例:
假设分量中有三个点
A
,
B
,
C
A, B, C
A,B,C,两条边
(
A
−
B
)
,
(
B
−
C
)
(A-B), (B-C)
(A−B),(B−C)。合法的选法如下:
- 边 ( A − B ) (A-B) (A−B) 选 A A A,边 ( B − C ) (B-C) (B−C) 选 B B B。
- 边 ( A − B ) (A-B) (A−B) 选 A A A,边 ( B − C ) (B-C) (B−C) 选 C C C。
- 边 ( A − B ) (A-B) (A−B) 选 B B B,边 ( B − C ) (B-C) (B−C) 选 C C C。
总共有 3 种方案,等于节点数 s [ i ] = 3 s[i] = 3 s[i]=3。
3. e [ i ] = s [ i ] e[i] = s[i] e[i]=s[i] 且 l o o p [ i ] = 0 loop[i] = 0 loop[i]=0 (单环可双向)
解释:
如果边数
e
[
i
]
e[i]
e[i] 等于节点数
s
[
i
]
s[i]
s[i],说明该分量是一个环。如果没有预占用点,则环上的路径可以正反两种方向行走,因此方案数为 2。
示例:
假设分量中有两个点
A
,
B
A, B
A,B,两条边
(
A
−
B
)
,
(
A
−
B
)
(A-B), (A-B)
(A−B),(A−B)。合法的选法有两种:
- 第一条边选 A A A,第二条边选 B B B。
- 第一条边选 B B B,第二条边选 A A A。
总共有 2 种方案。
4. e [ i ] = s [ i ] e[i] = s[i] e[i]=s[i] 且 l o o p [ i ] = 1 loop[i] = 1 loop[i]=1 (单环被钉死)
解释:
如果边数
e
[
i
]
e[i]
e[i] 等于节点数
s
[
i
]
s[i]
s[i],并且某个奇数点已经被预占用,则环的方向被固定,只能有一种选法。
示例:
假设分量中有两个点
A
,
B
A, B
A,B,两条边
(
A
−
B
)
,
(
A
−
B
)
(A-B), (A-B)
(A−B),(A−B),且点
A
A
A 已被预占用。唯一的合法选法是两条边都选
B
B
B。
总共有 1 种方案。
总结
根据以上分析,代码中对每个并查集根 i i i 的处理逻辑如下:
if (e[i] > s[i]) ans = 0; // 冲突,方案数为 0
else if (e[i] < s[i]) ans = ans * s[i] % mod; // 树结构,方案数为节点数
else /* e[i] == s[i] */ ans = ans * (loop[i] ? 1 : 2) % mod; // 环结构,根据是否有预占用点决定方案数
示例演示
示例 1
输入:
3 4 4
1 2
2 1
3 2
2 3
3 4
- 奇数点: ( 1 , 2 ) , ( 2 , 1 ) , ( 3 , 2 ) , ( 2 , 3 ) , ( 3 , 4 ) (1,2), (2,1), (3,2), (2,3), (3,4) (1,2),(2,1),(3,2),(2,3),(3,4)。
- 中间点候选:
- ( 1 , 2 ) − > ( 2 , 1 ) (1,2) -> (2,1) (1,2)−>(2,1):候选点 ( 1 , 1 ) , ( 2 , 2 ) (1,1), (2,2) (1,1),(2,2)。
- ( 2 , 1 ) − > ( 3 , 2 ) (2,1) -> (3,2) (2,1)−>(3,2):候选点 ( 2 , 2 ) , ( 3 , 1 ) (2,2), (3,1) (2,2),(3,1)。
- ( 3 , 2 ) − > ( 2 , 3 ) (3,2) -> (2,3) (3,2)−>(2,3):候选点 ( 3 , 3 ) , ( 2 , 2 ) (3,3), (2,2) (3,3),(2,2)。
- ( 2 , 3 ) − > ( 3 , 4 ) (2,3) -> (3,4) (2,3)−>(3,4):候选点 ( 2 , 4 ) , ( 3 , 3 ) (2,4), (3,3) (2,4),(3,3)。
- 合并后唯一分量有 5 个点,4 条边,无预占用环,方案数为 5 5 5。
输出:
5
复杂度分析
- 时间复杂度:每个测试用例的读入和处理时间为 O ( k ) O(k) O(k),并查集操作均摊为 O ( α ( n m ) ) O(α(nm)) O(α(nm))。总体复杂度为 O ( ∑ ( n m + k ) ) < = O ( 1 0 6 ) O(∑ (nm + k)) <= O(10^6) O(∑(nm+k))<=O(106)。
- 空间复杂度:主要为并查集数组,大小为 O ( n m ) O(nm) O(nm)。
参考代码:
#include<bits/stdc++.h>
#define int long long
#define all(x) x.begin(),x.end()
#define rall(x) x.rbegin(),x.rend()
#define pb push_back
#define pii pair<int,int>
using namespace std;
const int mod=1e9+7;
int qpw(int a,int b){int ans=1;while(b){if(b&1)ans=ans*a%mod;a=a*a%mod,b>>=1;}return ans;}
int inv(int x){return qpw(x,mod-2);}
struct DSU{
vector<int>f,siz;
DSU(){}
DSU(int n){
init(n);
}
void init(int n){
f.resize(n);
iota(all(f),0);
siz.assign(n,1);
}
int find(int x){
while(x!=f[x]){
x=f[x]=f[f[x]];
}
return x;
}
bool same(int x,int y){
return find(x)==find(y);
}
bool merge(int x,int y){
x=find(x);y=find(y);
if(x==y)return false;
siz[x]+=siz[y];
f[y]=x;return true;
}
int size(int x){return siz[find(x)];}
};
void solve(){
int n,m,k;cin>>n>>m>>k;
auto get=[&](int x,int y) {
return x*m+y;
};
vector<int>x(k+1),y(k+1);
for (int i=0;i<=k;i++)cin>>x[i]>>y[i],x[i]--,y[i]--;
DSU f(n*m);
vector<int>loop(n*m);
vector<int>e(n*m);
for (int i=0;i<=k;i++) {
int u=get(x[i],y[i]);
loop[u]=1;
e[u]++;
}
for (int i=1;i<=k;i++) {
int dx=abs(x[i]-x[i-1]),dy=abs(y[i]-y[i-1]);
if (dx+dy!=2) {
cout<<0<<'\n';return;
}
int a,b,c,d;
if (dx==2) {
a=(x[i]+x[i-1])/2;
b=y[i];
c=a;
d=b;
}else if (dy==2) {
a=x[i];
b=(y[i]+y[i-1])/2;
c=a;
d=b;
}else {
a=x[i],b=y[i-1];
c=x[i-1],d=y[i];
}
int u=get(a,b);
int v=get(c,d);
if (!f.same(u,v)) {
e[f.find(u)]+=e[f.find(v)];
loop[f.find(u)]|=loop[f.find(v)];
f.merge(u,v);
}
e[f.find(u)]++;
if (u==v) {
loop[f.find(u)]=1;
}
}int ans=1;
for (int i=0;i<n*m;i++) {
if (f.find(i)==i) {
int s=f.size(i);
if (e[i]>s) {
ans=0;
}else if (e[i]==s){
if (!loop[i]) {
ans=ans*2%mod;
}
}else {
ans=ans*s%mod;
}
}
}
cout<<ans<<'\n';
}
signed main(){
ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);
int _=1;
cin>>_;
while(_--)solve();
}