2025“钉耙编程”中国大学生算法设计暑期联赛(1)01/03/05/06/07/09/10

难度排序

1. 1010 中位数

知识点:优化枚举,动态维护中位数

两种做法

  1. 枚举区间中点+动态维护中位数,复杂度 n^2*logn(正常来说会TLE)
  2. 枚举每个数作为中位数时的贡献,复杂度 n^2

第一种做法

复杂度2000 * 2000 * 20 * log2000,大概 8e8 的复杂度,如果使用大常数的数据结构,例如多重集和优先队列动态维护中位数,会TLE

但因为树状数组天生的超级小常数,导致使用树状数组维护中位数的速度飞快,具体来说:

image

树状数组写法就比较无脑,每次我们枚举区间的终点 mid

则 [mid-1, mid+1], [mid-2, mid+2], ...... , [mid-k, mid+k]都是一个以 mid 为中点的序列

在序列每次变大时,将新加进来的数放入树状数组,查询中位数,累加答案即可

注意如果在树状数组上朴素的查询中位数,复杂度是 log*log 的,但是有方法可以在单 log 的时间下,在树状数组上查询第k大值

注意这个做法不是正解,只是被优秀的卡常技巧卡过去了

第二种做法(正解)

枚举每个数作为中位数时的答案:

当 a[i] 作为中位数时,枚举 j :从 i 到 n

设数组p,含义是:如果 a[j] > a[i] 则 p[j] = 1, a[j] < a[i] 则 p[j] = -1

求得 i~n 的p数组后,对 p[i~n] 做前缀和,设前缀和数组为pre[j]。

步骤1:则此时 pre[j] 表示:区间 i ~ j 还需要 pre[j] 个比 a[i] 小的数才能使得 a[i] 为区间中位数,假设 t=pre[i]

步骤2:我们用相同的方法,在区间 1~i 上,找到满足 j :1~i,a[j] ~ a[i] 恰好需要 t 个比 a[i] 大的数。

此时,这步骤1,2的两个 j ,正好是一个以 a[i] 作为中位数的合法区间。累加答案即可

代码非常简单好写,甚至不用创建新数组

正解代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
//using i128 = __int128_t;
const ll inf = 1e18;
const int mod = 998244353;

void solve(){
    int n;
    cin>>n;

    vector<int> a(n+1);
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }

    int ans=0;

    for(int i=1;i<=n;i++){
        //枚举a[i]做为中位数
        vector<int> cnt(n+2010);
        int now=0;

        for(int j=i;j<=n;j++){
            if(a[j]>a[i]) now++;
            else if(a[j]<a[i]) now--;
            cnt[now+2000]+=j;
        }

        now=0;

        for(int j=i;j>=1;j--){
            if(a[j]<a[i]) now++;
            else if(a[j]>a[i]) now--;
            ans+=j*cnt[now+2000]*a[i];
        }
    }
    cout<<ans<<endl;
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int ct=1;
    cin>>ct;
    while(ct--){
        solve();
    }
    return 0;
}

树状数组代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
//using i128 = __int128_t;
const ll inf = 1e18;
const int mod = 998244353;

struct TreeArray {
    int n;
    vector<ll> tr;

    TreeArray(int _n) { init(_n); }
    TreeArray() {}

    // 初始化,开辟大小为 n+1 的数组,下标从 1 开始使用
    void init(int _n) {
        n = _n;
        tr.assign(n + 1, 0);
    }

    // lowbit 函数,获取最低位的 1
    inline int lowbit(int x) {
        return x & -x;
    }

    // 在下标 x 增加值 c
    void insert(int x, ll c) {
        for (int i = x; i <= n; i += lowbit(i)) {
            tr[i] += c;
        }
    }

    // 获取前缀和 [1..x]
    ll sum(int x) {
        ll ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) {
            ans += tr[i];
        }
        return ans;
    }

    ll query(int l,int r){
        return sum(r)-sum(l-1);
    }

    // 二分查找:返回最大的 x,使得 sum(x) <= k
    // 如果所有前缀和都大于 k,则返回 0
    int select(ll k) const {
        int x = 0;
        ll cur = 0;
        // i 从最高 2^⌊lg n⌋ 开始
        for (int i = 1 << (31 - __builtin_clz(n)); i > 0; i >>= 1) {
            if (x + i <= n && cur + tr[x + i] <= k) {
                x += i;
                cur += tr[x];
            }
        }
        return x;
    }

    int kth(int k) const {
        int pos = 0, bit = 1<<11; 
        // 2^11 = 2048 > 2000,上界可稍微放大
        for (; bit; bit >>= 1) {
            int np = pos + bit;
            if (np <= n && tr[np] < k) {
                k -= tr[np];
                pos = np;
            }
        }
        return pos + 1;
    }
};

void solve(){
    int n;
    cin>>n;
    
    vector<int>a(n+1);
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }

    ll ans=0;

    for(int mid=1;mid<=n;mid++){

        TreeArray tr(2000);
        tr.insert(a[mid],1);

        int med=a[mid];
        vector<int> cnt(n+1);
        cnt[med]=1;

        int l=0,r=0;

        ans+=mid*mid*med;

        int maxk=min(mid-1,n-mid);


        for(int k=1;k<=maxk;k++){
            tr.insert(a[mid-k],1);
            tr.insert(a[mid+k],1);

            med=tr.kth(k+1);
            ans+=med*(mid-k)*(mid+k);
        }
    }
    cout<<ans<<"\n";
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int ct=1;
    cin>>ct;
    while(ct--){
        solve();
    }
    return 0;
}

2. 1005 传送门

知识点:图论,最短路,建图,虚点

重点是如何建立可以直接跑最短路的图模型

如果你做过类似的题目,就会很快的发现,做法大概是要建立额外的点,辅助进行最短路

回顾题意,对于连续且所属同一协会的路径,只需要交一次钱即可

假设路径u~v,协会是c

则对点u,建立两个虚点,分别表示:状态{u,c} (id1),状态{v,c} (id2)

连接:

u 到 id1 的路径,边权为1,id1 到 u 的路径,权值为0

v 到 id2 的路径,边权为1,id2 到 v 的路径,权值为0

id1 到 id2 的双向路径,权值为0

建立图后,你会发现,刚好满足题意,对于连续且所属同一协会的路径,只需要交一次钱

而我们建立的虚点,等价于:在一段连续且协会相同的路径上,只需要一次缴费,即可走到由我们自定义的状态表示的点上

而这些点,可以免费到达连续且协会相同的其他状态点,也可以免费下到原图上的点

同时因为是01图,不需要 dijkstra,跑 01BFS 即可

#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
//using i128 = __int128_t;
const ll inf = 1e18;
const int mod = 998244353;

struct edge{
    int u,v,c;
};

void solve(){
    int n,m;
    cin>>n>>m;

    int tar=n;

    vector<vector<pii>> g(2*(n+m+10));
    map<pii,int> id;
    vector<edge> e;

    for(int i=1;i<=m;i++){
        int u,v,c;
        cin>>u>>v>>c;
        if(!id[{u,c}]) id[{u,c}]=++n;
        if(!id[{v,c}]) id[{v,c}]=++n;
        e.push_back({u,v,c});
    }

    
    for(auto [u,v,c]:e){
        int id1=id[{u,c}];
        int id2=id[{v,c}];

        g[u].push_back({id1,1});
        g[v].push_back({id2,1});
        g[id1].push_back({id2,0});
        g[id2].push_back({id1,0});
        g[id1].push_back({u,0});
        g[id2].push_back({v,0});
    }
    

    deque<int> dq;
    vector<int> dist(n+1,inf);
    dist[1]=0;
    dq.push_back(1);

    while(!dq.empty()){
        int u=dq.front();
        dq.pop_front();
        for(auto[v,w]:g[u]) {
            if(dist[v]>dist[u]+w) {
                dist[v]=dist[u]+w;
                if(w==0) dq.push_front(v);
                else dq.push_back(v);
            }
        }
    }

    cout<<dist[tar]<<endl;
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int ct=1;
    cin>>ct;
    while(ct--){
        solve();
    }
    return 0;
}

3. 1009 子序列

知识点:优化枚举

从 n 到 1,枚举两端点值中的最小值m。(或者说枚举可选子序列的最大值m,这个是更符合下面说法的)

同时维护所有大于等于m的值的集合p

枚举到m时,将m加入到集合p中,此时两端点一定从p中选,因为不从p中选,则两端点最小值小于m,不符合我们的枚举方式

而如果从p中选,则因为此时最小值是m,所以中间可选的所有值一定小于m,所以一定不会选到集合中的值

所以,最优选法一定是,两端点分别为集合中最靠左的数(位置是l)和最靠右的数(位置是r),子序列长度是:

(r-l+1)- p.size + 2

r-l+1 是区间长度,减去 p.size 是因为这些数都不可选,+2是两端点

#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
//using i128 = __int128_t;
const ll inf = 1e18;
const int mod = 998244353;

void solve(){
    int n;
    cin>>n;

    vector<int> a(n+1),pos(n+1);

    for(int i=1;i<=n;i++){
        cin>>a[i];
        pos[a[i]]=i;
    }

    if(n==1){
        cout<<1<<endl;
        return;
    }

    int ans=1;
    int r=0,l=n+1;
    int cnt=0;
    
    for(int i=n;i>=1;i--){
        //枚举两个端点的最小值为i的情况
        cnt++;
        r=max(r,pos[i]);
        l=min(l,pos[i]);

        if(cnt>=2){
            ans=max(ans,(r-l+1)-cnt+2);
        }
    }
    cout<<ans<<endl;
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int ct=1;
    cin>>ct;
    while(ct--){
        solve();
    }
    return 0;
}

4. 1006 景区建设

知识点:好像没啥知识点,硬要说的话能沾上点最小生成树

首先对于每一个“山峰”和(1,1),必须要建立传送器。同时因为建立传送器的代价很大,所以一定是要最小化建立传送器的数量。

所以传送器的数量可以确定,就是山峰的数量,同时还需要包含(1,1)

然后就是在这些传送器之间,建立路径(最小生成树),使得传送器全联通。

路径的代价是:
image

即使 x1 和 x2的差值加上 y1 和 y2 的差值拉到最大,也不大与919810,所以影响建立路径的唯一因素是高度差。

所以对所有传送器按照高度排序,相邻的传送器之间建立路径即可

#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
//using i128 = __int128_t;
const ll inf = 1e18;
const int mod = 998244353;

struct node{
    int x,y,h;
};

void solve(){
    int n,m;
    cin>>n>>m;

    vector<vector<int>> g(n+1,vector<int>(m+1));

    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            cin>>g[i][j];
        }
    }

    vector<node> t;

    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            bool f=1;
            if(i>1 && g[i][j]<g[i-1][j]) f=0;
            if(i<n && g[i][j]<g[i+1][j]) f=0;
            if(j>1 && g[i][j]<g[i][j-1]) f=0;
            if(j<m && g[i][j]<g[i][j+1]) f=0;
            if(i==1 && j==1) f=1;

            if(f){
                t.push_back({i,j,g[i][j]});
            }
        }
    }

    int ans=(1ll<<34)*(t.size()-1);

    sort(t.begin(),t.end(),[&](node t1,node t2){
        return t1.h<t2.h;
    });

    for(int i=1;i<t.size();i++){
        ans+=abs(t[i].h-t[i -1].h)*919810;
        ans+=abs(t[i].y-t[i-1].y)*5141;
        ans+=abs(t[i].x-t[i-1].x)*114;
    }

    cout<<ans<<endl;

}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int ct=1;
    cin>>ct;
    while(ct--){
        solve();
    }
    return 0;
}

5. 1007 树上LCM

知识点:SOSDP,分解质因数,LCM

题解1:

首先对于 X,可以分解成很多个质因数的几次方相乘的形式,例如 504 = 2^3 * 3^2 * 7^1

而对于 X 的数据范围,最多大概是7~8种不同的质因数相乘,设 X 不同的质因数个数为 k

把每个节点的值压缩成 k 位二进制形式:

假设 X 的第 i 个质因子的值为 val,出现次数为 cnt,则可以表示为:val^cnt 的形式

对于 a[u],如果也存在因子 val,且出现次数恰好等于 cnt,则 a[u] 对应的二进制值的第 i 位为 1

如果 a[u] 存在因子 val 但出现次数大于 cnt,或者 a[u] 有 X 中不存在的因子,则直接给 a[u] 的二进制值改成 -1,表示 a[u] 不能出现在任何一条路径中

(因为在这两种情况下,任何数与 a[u] 的 LCM 不可能等于 X)

将每个数转换为二进制掩码后,开始计算路径数量

对于节点 u,子节点为 v1, v2, v3, ... vi

可能的路径一共有三种:

  1. 以 u 为上端点,另一端点在子节点的子树内部
  2. 两端点都在子节点的子树内部,且路径会跨越 u
  3. u->u 的一条路径 (节点自身)

设 f[u] [mask] 表示:以u为一个端点,另一个端点在子树内部,路径的掩码或起来为 mask 的路径数量

对于一条合法的路径,等价于路径的 mask 或值 == (1<<k)-1

对子节点 vi,计算 u 的第2种路径时,分为两步:

  1. 先枚举 vi 子树内的所有以 vi 为一个端点的路径掩码 mask1,我们要找到其他子树 v1 到 v(i-1) 连接到 u 后(情况 1 的路径),路径掩码 mask2 与 mask1 或起来为(1<<k)-1的路径数量。
    我们可以计算出 mask2 的值,mask2 为 (mask1 ^ (1<<k)-1) 的超集。所以对于 f[vi][mask1],符合条件的可连接路径情况2数量为:f[u] 中((mask1 ^ (1<<k)-1)的所有超集之和,这一步可用 sosdp 计算
  2. 计算完情况2的路径后,再将 f[v1] [mask] 合并到 u上,更新 f[u] [mask],统计情况1路径的同时为下一个子节点 v(i+1) 的情况2路径统计做准备

整个过程用 DFS 维护即可,单点复杂度:k *(2^k),总体复杂度n * k *(2^k)

题解2:

\(x\) 分解质因数,可以分解为最多不超过 8 个质因数相乘。

我们设 \(x\) 的质因子个数为 \(k\) 个,然后把每个节点 \(a_i\) 的值状压成一个二进制数:

如果 \(a_i\) 的某个质因子 \(t\) 出现次数与 \(x\)\(t\) 出现次数相等,则将这一位修改为 \(1\)

\(a_i\) 的某个质因子 \(t\) 出现次数小于 \(x\)\(t\) 出现次数,则将这一位修改为 \(0\)

\(a_i\) 的某个质因子 \(t\) 出现次数大于 \(x\)\(t\) 出现次数,则将这一位修改为 \(-1\)\(-1\) 代表这个点不能在 \(LCM\) 路径上,因为不可能 LCM 出 \(X\)

\(a_i\) 的某个质因子 \(t\)\(x\) 中没有出现过,则将这一位修改为 \(-1\)

然后 \(dfs\) 整棵树

\(f[u][j]\) 表示:以 \(u\) 为一个端点,另一个端点在子树内部,路径的掩码或起来为 \(j\) 的路径数量

\(dfs\) 节点 \(u\)时,找到所有子节点 \(v\) 内部的 \(f[v][mask]\)

再把 \(f[v][mask]\) 合并到 \(u\)

合并时,假设子树 \(v\) 内有个掩码为 \(mask\) 的路径

则这个路径可以和 \([(mask~~xor~~((1<<k)-1))]\) 这个掩码 \(|\) 来得到 \([1<<k-1]\)

\([(mask~~xor~~((1<<k)-1))]\) 表示 \(mask\)\(full mask~~((1<<k)-1)\) 的补集

同时,还可以和 \((mask~~xor~~((1<<k)-1))\) 的所有超集,\(|\) 得到 \([1<<k-1\)]

所以这里可以使用超集和DP(SOSDP的变形)来得到超集之和

统计完子树 \(v\) 的答案后再将 \(f[v]\) 合并到 \(f[u]\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
//using i128 = __int128_t;
const ll inf = 1e18;
const int mod = 998244353;

int lcm(int a,int b){
    return a*b/__gcd(a,b);
}

//先处理出x的质因数和个数
    //dfs每个点u,找到所有v内部的f[mask]
    //再把fv[mask]合并到u上
    //合并时,假设子树v内有个掩码为mask的路径
    //则这个路径可以和: (mask^((1<<k)-1)) 这个掩码|起来得到1<<k-1
    //但同时,还可以和(mask^((1<<k)-1))的所有超集,|得到1<<k-1
    //所以可以使用高维前缀和,sosdp

void solve(){
    int n,x;
    cin>>n>>x;

    vector<vector<int>> g(n+1);
    for(int i=1;i<n;i++){
        int u,v;
        cin>>u>>v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    vector<int> a(n+1);
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }

    map<int,int> cnt;//统计x的质因数对应的个数

    //统计cnt
    int nx=x;
    for(int i=2;i<=nx;i++){
        int tmp=0;
        while(nx%i==0){
            nx/=i;
            tmp++;
        }
        if(tmp) cnt[i]=tmp;
    }
    if(nx>1){
        cnt[nx]=1;
    }

    int k=cnt.size();//质因数种类数

    auto get=[&](int val)-> int {
        if(lcm(x,val)!=x) return -1;

        int pos=0,res=0;

        for(auto [v,c]:cnt){
            int now=0;
            while(val%v==0){
                now++;
                val/=v;
            }
            if(now==c) res|=(1<<pos);
            pos++;
        }
        return res;
    };

    for(int i=1;i<=n;i++){
        a[i]=get(a[i]);// 将a[i]转换为与x的质因数对应的掩码
    }

    int ans=0;
    //以u为一个端点,另一个端点在子树内部,路径的掩码或起来为j的路径数量
    vector<vector<int>> f(n+1,vector<int>(1<<k));

    auto dfs=[&](auto dfs,int u,int pre)->void {
        for(auto v:g[u]){
            if(v==pre) continue;
            dfs(dfs,v,u);
        }

        if(a[u]==-1) return;

        f[u][a[u]]=1;
        for(auto v:g[u]){
            if(v==pre) continue;
            
            //计算超集和
            auto s=f[u];
            for(int j=0;j<k;j++){
                for(int i=0;i<(1<<k);i++){
                    if(!(i>>j&1)){
                        s[i]+=s[i^(1<<j)];
                    }
                }
            }

            for(int i=0;i<(1<<k);i++){
                int val=i|a[u];
                ans+=s[val^((1<<k)-1)]*f[v][i];
                f[u][val]+=f[v][i];
            }
        }
        //之前只统计了,从子树v连接上去的路径,没有统计单个节点的
        if(a[u]==(1<<k)-1) ans++;
    };

    dfs(dfs,1,0);
    cout<<ans<<endl;
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int ct=1;
    cin>>ct;
    while(ct--){
        solve();
    }
    return 0;
}

6. 1003 奸商

知识点:二进制枚举,SOSDP

所有长度为奇数的字符串一定优秀

若所有长度为 \(n\)\(n\) 为偶数的子串优秀,则所有长度大于 \(n\) 且是偶数的子串优秀

所以只要保证所有最短的偶数子串优秀即可

最短的长度是 \(len\),若 \(len\) 是奇数,则 \(len++\)

将所有长度为 \(len\) 的子串,幻视需求表达为一个 \(17\) 位二进制数,若将 \('a'+i\) 幻视后,子串可以变优秀,则第 \(i\) 位修改为 \(1\),否则为 \(0\)

我们需要找到一个方案,让所有的需求,至少有一个被改为 \(1\) 的位进行幻视

暴力做法:

枚举 \(0 ~到~ (1<<17-1)\) 的所有方案,若某个方案可以满足所有需求,则这个方案可以作为备选,复杂度 \(n * (1<<17) == 4e8\)

SOSDP做法:

设需求 \(m\) 对于全集的补集 \(t\)

如果修改方案为 \(t\)\(t\) 的子集,则需求 \(m\) 不能被修改到,所以方案 \(t\)\(t\) 的子集不能做为备选方案

所以对每个需求 \(m\),可以找到一个 \(t\),将 \(t\) 标记为 \(false\),再使用超集和DP,将 \(t\) 的所有子集也标记为 \(false\),其他没被标记的集合可作为备选

复杂度\(17 * (1<<17)\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
//using i128 = __int128_t;
const ll inf = 1e18;
const int mod = 998244353;

void solve(){
    int n;
    cin>>n;
    string s;
    cin>>s;
    s=" "+s;

    vector<int> w(200);
    for(int i=0;i<17;i++){
        int val;
        cin>>val;
        w['a'+i]=val;
    }

    int len;
    cin>>len;

    if(len&1) len++;

    vector<int> nd;
    for(int i=1;i<=n-len+1;i++){
        int mask=0;
        for(int l=i,r=i+len-1;l<r;l++,r--){
            if(s[l]==s[r]){
                mask=0;
                break;
            }
            else{
                int k=l;
                if(s[r]<s[l]) k=r;
                mask|=1<<(min(s[l]-'a',s[r]-'a'));
            }
        }
        if(mask){
            nd.push_back(mask);
        }
    }

    //到这里开始,sosdp或暴力
    //要从0~(1<<17)中找一个方案,使得方案|上任何一个nd的结果都不为0
    //暴力做法复杂度nd.size()(==3000) * (1<<17) == 4e8
    int ans=inf;
    if(nd.size()==0) ans=0;
    
    // for(int i=0;i<(1<<17);i++){
    //     int cost=0,f=1;
    //     for(auto m:nd){
    //         if(!(i&m)){
    //             f=0;
    //             break;
    //         }
    //     }
    //     if(f){
    //         for(int k=0;k<17;k++){
    //             if(i>>k&1) cost+=w['a'+k];
    //         }
    //         ans=min(ans,cost);
    //     }  
    // }
    // cout<<ans<<endl;

    //sosdp做法,对于所有nd的补集t,标记为false
    //然后做超集和dp,剔除掉所有t的子集

    vector<int> f(1<<17,1);
    for(auto m:nd){
        f[m^((1<<17)-1)]=0;
    }

    for(int j=0;j<17;j++){
        for (int mask=0;mask<(1<<17);mask++){
            if(!(mask>>j&1)){
                f[mask]&=f[mask^(1<<j)];
            }
        }
    }

    for(int mask=0;mask <(1<<17);mask++){
        if(!f[mask]) continue;
        int cost=0;
        for(int k=0; k<17;k++){
            if(mask & (1<<k)) cost+=w['a'+k];
        }
        ans=min(ans,cost);
    }
    cout<<ans<<endl;

}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int ct=1;
    cin>>ct;
    while(ct--){
        solve();
    }
    return 0;
}

7. 1001 博弈

知识点:博弈论,nim游戏,反nim游戏,组合数学

(前置知识是 \(nim\)\(anti-nim\) 的结论以及组合数学)

每个房间可以看成是一个 \(nim\) 游戏或一个反 \(nim\) 游戏

称先手为 \(A\),后手为 $B

看成 \(nim\) 游戏时:

异或和非零:则 \(A\) 可以让下一个房间B先手

异或和为零:则 \(B\) 可以让下一个房间A先手

看成反 \(nim\) 游戏时:

若所有堆都是 \(1\),且堆的数量是偶数,则下一个房间 \(A\) 先手

若至少一堆大于 \(1\),且异或和非零,则 \(A\) 可以让下一个房间 \(A\) 先手

其余情况,\(B\) 可以让下一个房间B先手

\(nim\)\(anti-nim\) 的结果是确定的

所以每个房间都是以下四种结果之一:

  1. \(A\) 可以让下个房间B先手,\(A\) 可以让下个房间自己先手
  2. \(B\) 可以让下个房间A先手,\(B\) 可以让下个房间自己先手
  3. \(A\) 可以让下个房间B先手,\(B\) 可以让下个房间自己先手
  4. \(A\) 可以让下个房间自己先手,\(B\) 可以让下个房间自己先手

对于结果1,2

结果1:如果到这个房间,则 \(A\) 必胜

结果2:如果到这个房间,则 \(B\) 必胜

结果3:到这个房间,一定会交换下一个房间的先后手顺序

结果4:一定不会交换下一个房间的先后手顺序

总结归纳房间类型:

a:先手可以决定下一个房间的先后手

b:后手可以决定下一个房间的先后手

c:先后手交换顺序

d:先后手顺序不变

排列组合 \(Alice\) 赢的情况:

一.\(a\)\(b\) 不为0的情况

  1. 前面有偶数个 \(c\),然后放一个 \(a\),剩下的 \(a,b\) 随便放( \(d\) 先不考虑,因为 \(d\) 对答案完全没影响)
  2. 前面有奇数个 \(c\) ,然后放一个 \(b\) ,剩下的 \(a,b\) 随便放
  3. 累加这两种情况,再乘上 \(C(n,d)*d!\)

二.\(a\)\(b\) 都为 \(0\) 的情况

\(c\) 是偶数时答案为 \(n!\) ,否则答案为 \(0\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
//using i128 = __int128_t;
const ll inf = 1e18;
const int mod = 1e9+7;

const int N=1e6;
int fact[N+1],infact[N+1];

int qmi(int a,int b,int p){
    int res=1;
    while(b){
        if(b&1) res=res*a%p;
        b>>=1;
        a=a*a%p;
    }
    return res;
}

// 组合数计算 C(n, k)
ll C(int a,int b){
    if(a<0 || b<0 || b>a) return 0;
    return fact[a]*infact[b]%mod*infact[a-b]%mod;
}

void init(){
    fact[0]=1;
    infact[0]=1;
    for (int i=1;i<=N;i++){
        fact[i]=fact[i-1]*i%mod;
    }
    infact[N]=qmi(fact[N],mod-2,mod);
    for(int i=N-1;i>=1;i--){
        infact[i]=infact[i+1]*(i+1)%mod;
    }
}

void solve(){
    int n;
    cin>>n;
    int a=0,b=0,c=0,d=0;

    for(int i=1;i<=n;i++){
        int k,val,sum=0;
        cin>>k;
        bool f=1;//是否全0

        for(int j=1;j<=k;j++){
            cin>>val;
            sum^=val;
            if(val!=1) f=0;
        }
        
        bool A=0;
        if(f && !(k&1)) A=1;
        else if(!f && sum) A=1;

        if(sum && A) a++;
        else if(!sum && !A) b++;
        else if(sum && !A) c++;
        else d++;
    }

    int ans=0;
    if (a==0 && b==0){
        if(c&1) cout<<fact[n]<<endl;
        else cout<<0<<endl;
        return;
    }

    /*
    1. 前面有偶数个c,然后放一个a,剩下的ab随便放(d先不考虑,因为d对答案完全没影响)
    2. 前面有奇数个c,然后放一个b,剩下的ab随便放
    累加这两种情况,再乘上C(n,d)*d!
    */
    for(int i=0;i<=c;i+=2){
        ans+=C(c,i)*fact[i]%mod*a%mod*fact[n-d-1-i]%mod;
        ans%=mod;
    }
    for(int i=1;i<=c;i+=2){
        ans+=C(c,i)*fact[i]%mod*b%mod*fact[n-d-1-i]%mod;
        ans%=mod;
    }

    ans*=C(n,d)*fact[d]%mod;
    ans%=mod;
    cout<<ans%mod<<endl;
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    init();

    int ct=1;
    cin>>ct;
    while(ct--){
        solve();
    }
    return 0;
}

前面的地图以后再来探索吧

posted @ 2025-07-19 04:13  LYET  阅读(623)  评论(7)    收藏  举报