Codeforces Round 1021 (Div. 1)-B. Baggage Claim


题目解析

本题的核心是通过已知的奇数位置点 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。路径需要满足以下条件:

  1. 每个相邻点 p i pi pi p i + 1 pi+1 pi+1 必须共享一条边。
  2. 路径不能自交(即路径上的点互不相同)。

我们需要计算所有可能的路径补全方案数,并对结果取模 1 0 9 + 7 10^9 + 7 109+7


建模思路

核心建模
  • 奇数点和偶数点的关系:每一对奇数点 p 2 i − 1 p2i-1 p2i1 p 2 i + 1 p2i+1 p2i+1 之间必须插入一个中间点 p 2 i p2i p2i,且 p 2 i p2i p2i 必须与 p 2 i − 1 p2i-1 p2i1 p 2 i + 1 p2i+1 p2i+1 相邻。
  • 候选点的选择
    • 如果 p 2 i − 1 p2i-1 p2i1 p 2 i + 1 p2i+1 p2i+1 在同一行或列上相距 2,则有唯一的一个中点作为候选。
    • 如果 p 2 i − 1 p2i-1 p2i1 p 2 i + 1 p2i+1 p2i+1 呈“对角”关系(行列各差 1),则有两个候选点。
  • 图模型:将所有候选点视为图中的节点,每一段 ( p 2 i − 1 , p 2 i + 1 ) (p2i-1, p2i+1) (p2i1,p2i+1) 的候选点之间连一条边(如果候选点相同,则形成自环)。问题转化为在这张图中选择每个边的一个端点,使得不同边选择的端点互不相同。

图的性质

  1. 节点数:最多为 n × m n × m n×m
  2. 边数:最多为 k + ( k + 1 ) k + (k+1) k+(k+1)
  3. 连通分量:每个连通分量要么是一棵树,要么包含一个环。

并查集统计法

我们使用并查集(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) (AB),(AB),(AB)。无论如何选择端点,都会导致重复选点的情况。因此,方案数为 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) (AB),(BC)。合法的选法如下:

  • ( A − B ) (A-B) (AB) A A A,边 ( B − C ) (B-C) (BC) B B B
  • ( A − B ) (A-B) (AB) A A A,边 ( B − C ) (B-C) (BC) C C C
  • ( A − B ) (A-B) (AB) B B B,边 ( B − C ) (B-C) (BC) 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) (AB),(AB)。合法的选法有两种:

  • 第一条边选 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) (AB),(AB),且点 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();
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值