2025 首届河北大学程序设计竞赛暨新生赛

赛前预计难度:

M<FGL<BCEHK<DIJ<A

赛时实际难度:

M. 欢迎参加第一届河北大学程序设计竞赛

直接输出字符串即可

G. 别打算法竞赛了,先吃饭吧

计算出所有窗口每分钟可以做 \(sum\) 份饭,计算 \(\lceil x/sum \rceil\) 即可

c++ 中 \(\lceil a/b \rceil\) 上取整计算方式: \((a+b-1)/b\)

代码:

void solve(){
	int n;
    ll x;
    ll sum=0;

    cin>>n>>x;

    for(int i=1;i<=n;i++){
        int val;
        cin>>val;
        sum+=val;
    }

    cout<<(x+sum-1)/sum;
}

L. 书架

方法一:

使用优先队列(堆)维护所有值,每次取出最大值更新即可,时间复杂度:\(O(n*logn*logn)\)

方法二:

对所有值不断更新,记录下每次更新的减少量,将所有值更新到等于0后,将所有减少量排序,从大到小取即可

证明:对其中任意一个数,不断更新(除以2),最多更新 \(log_2^n\) 次,即总共得到 \(n*logn\) 个更新量,而对同一个数而言,一个大的更新量一定比一个小的更新量先出现。

复杂度 \(O(n*logn*logn)\)

详细题解:

要把总数降低到 s,且操作次数最小,所以可以想到,每次操作时,都让减少量尽可能的大。

  1. 如何让减少量尽可能的大?可以每次找到所有数的最大值,对这个数进行操作。

  2. 如何找到所有数的最大值?

    每次对所有数 \(for\) 循环,以 \(O(n)\) 的复杂度找到最大值

    考虑这种方法在最坏情况下的时间复杂度,假设 \(s\)\(0\),则需要把每个数都一直除二除到零,此时每个数会被操作 \(log\) 次。

    所以 \(n\) 个数就是操作 \(n*log\) 次,而每次操作,如果都 \(O(n)\) 的找最大值,则总复杂度是 \(O(n^2 log)\),显然会超时

    考虑如何优化找最大值的过程

    可以使用 \(stl\) 中的一个工具:优先队列(priority_queue)

    优先队列支持的功能是:

    1. 可以在 \(O(1)\) 的时间内找到最小值或最大值
    2. 同时支持在 \(O(logn)\) 的时间内插入一个元素
    3. \(O(logn)\) 的时间内删除最小值或最大值
    4. 详细的介绍:https://www.bilibili.com/video/BV1TN4y137Jx

​ 所以,可以将所有数都放入优先队列中,每次找到队列中的最大值,取出,操作,统计,再把操作后的数放回优先队列中即可

代码:

void solve(){
	int n,s;
    ll sum=0;
    cin>>n>>s;

    priority_queue<int> q;

    for(int i=1;i<=n;i++){
        int val;
        cin>>val;
        sum+=val;
        q.push(val);
    }

    int ans=0;

    while(sum>s){
        int val=q.top();
        q.pop();

        ans++;

        int diff=val-val/2;
        sum-=diff;
        q.push(val/2);
    }
    
    cout<<ans;
}

F. 回文文回

维护一个变量 \(x\),表示当前有多少个位置需要修改

对每次操作:

如果操作前无需修改,操作后无需修改,则 \(x\) 不变

如果操作前无需修改,操作后需要修改,则 \(x\) 加一

如果操作前需要修改,操作后无需修改,则 \(x\) 减一

如果操作前需要修改,操作后需要修改,则 \(x\) 不变

代码:

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

    string s;
    cin>>s;

    s=" "+s;//让s的下标从1开始

    int x=0;
    for(int i=1;i<=n/2;i++){
        if(s[i]!=s[n-i+1]){
            x++;
        }
    }

    while(q--){
        int pos;
        char ch;
        cin>>pos>>ch;

        int st=(s[pos]==s[n-pos+1]);
        s[pos]=ch;
        int ed=(s[pos]==s[n-pos+1]);

        if(st==ed){
            cout<<x<<endl;
        }
        else{
            if(st==1 && ed==0){
                x++;
            }
            else if(st==0 && ed==1){
                x--;
            }
            cout<<x<<endl;
        }
    }

}

B. 经典子数组

首先对原数组 \(a\) 计算前缀和数组 \(s\)

此时,\(a[i...j] ~mod~k==0\) 等价于:\(s[i-1]~mod~k==s[j]~mod~k\)

对每个位置 \(i\),统计从 \(1\)\(i\) 有多少个位置 \(j\),满足 \(s[j-1]~mod~k==s[i]~mod~k\) 即可

统计的过程可以用 \(map\) 记录并维护前缀所有的 \(s[i]~mod~k\) 的值

一遍 \(for\) 循环即可实现

复杂度 \(O(n)\)\(O(n*logn)\)

代码:

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

    ll ans=0;

    map<int,ll> mp;

    vector<ll> a(n+1),pre(n+1);

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

    for(int i=1;i<=n;i++){
        mp[pre[i-1]%k]++;
        ans+=mp[pre[i]%k];
    }
    cout<<ans;

}

C. 图书馆迷宫

使用 BFS 找到每个空地属于哪个连通块,对连通块标号,每个空地记录下其所属的连通块标号。

同时统计每个联通块的大小

对每个障碍,找到上下左右的空地,所属的不同连通块及其连通块大小,累加答案即可

也可使用并查集实现

时间复杂度 \(O(n*m)\)

代码:

#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;
const ll inf = 1e18;
const int mod = 998244353;

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

    vector<vector<char>> g(n+1,vector<char>(m+1));
    vector<vector<int>> ans(n+1,vector<int>(m+1));
    // 修正 st 的类型为 int,用于存储连通块 id
    vector<vector<int>> st(n+1,vector<int>(m+1,0));

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

    // 标记并统计每个连通块的大小,连通块只针对 '.' 计算
    vector<int> comp_size; // comp_size[id] = size, id 从 1 开始
    comp_size.push_back(0); // 占位,使得 id 与下标对齐
    int comp_id = 0;

    auto bfs = [&](auto bfs, int sx, int sy)-> void {
        // 只对未标记且为 '.' 的格子进行 bfs
        queue<pii> q;
        vector<pii> cells;
        q.push({sx,sy});
        st[sx][sy] = -1; // 临时标记为访问中
        cells.push_back({sx,sy});
        while(!q.empty()){
            auto [x,y] = q.front(); q.pop();
            const int dx[4] = {1,-1,0,0};
            const int dy[4] = {0,0,1,-1};
            for(int d=0;d<4;d++){
                int nx = x + dx[d];
                int ny = y + dy[d];
                if(nx>=1 && nx<=n && ny>=1 && ny<=m){
                    if(g[nx][ny]=='.' && st[nx][ny]==0){
                        st[nx][ny] = -1;
                        q.push({nx,ny});
                        cells.push_back({nx,ny});
                    }
                }
            }
        }
        // 给这个连通块分配 id 并记录大小
        comp_id++;
        int sz = (int)cells.size();
        comp_size.push_back(sz);
        for(auto &p: cells){
            st[p.first][p.second] = comp_id;
        }
    };

    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(g[i][j]=='.' && st[i][j]==0){
                bfs(bfs,i,j);
            }
        }
    }

    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(g[i][j]=='.'){
                ans[i][j] = 0;
            }else{
                // 收集相邻不同连通块 id
                set<int> s;
                const int dx[4] = {1,-1,0,0};
                const int dy[4] = {0,0,1,-1};
                for(int d=0;d<4;d++){
                    int nx = i + dx[d];
                    int ny = j + dy[d];
                    if(nx>=1 && nx<=n && ny>=1 && ny<=m){
                        int id = st[nx][ny];
                        if(id>0) s.insert(id);
                    }
                }
                long long sum = 1;
                for(int id: s) sum += comp_size[id];
                ans[i][j] = (int)sum;
            }
        }
    }

    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            cout<<ans[i][j];
            if(j<m) cout<<' ';
        }
        cout<<"\n";
    }
}

signed main(){

	ios::sync_with_stdio(0);
	cin.tie(0);

	int t=1;
	// cin>>t; 
	
	while(t--){
		solve(); 
	}

}

E. 数位DP

原题的数据范围是 \(1e18\),修改成 \(1e9\) 的目的是,让无论是新生还是老生,都可以有对应的办法解决此题

方法一:DFS \(1e9\) 范围内所有的幸运数,并判断其是否是超级幸运数,将答案统计到数组中即可

DFS 复杂度 \(O(能过)\)

如果不会 DFS?

方法二:直接暴力枚举 \(1-1e9\) 所有数字,判断其是否是幸运数和超级幸运数

复杂度 \(O(n*logn)\)

显然不能直接通过,但可以在本地运行程序(打表),计算出所有超级幸运数,直接复制到答题代码中。

经过实际测试,打表程序可以在一分钟以内计算出结果

打表代码:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;

bool check(ll val){
    int pre=10;
    while(val){
        int t=val%10;//每次获取 val 当前的最低位数值
        val/=10;
        if(t>pre) return 0;//如果当前位的数值比更底位的数值要大,则不满足递增,返回0 
        pre=t;
    }

    return 1;
}


void solve(){
    int n=1000000000;

    vector<int> ans;
    
    for(ll i=1;i<=n;i++){
        if(check(i) && check(i*i)){
            ans.push_back(i);
        }
    }
    
    for(auto val:ans){
        cout<<val<<" ";
    }

}

signed main(){

	ios::sync_with_stdio(0);
	cin.tie(0);

	int t=1;
	// cin>>t; 
	
	while(t--){
		solve(); 
	}

}

得到所有超级幸运数结果:

1,2,3,4,5,6,7,12,13,15,16,17,34,35,37,38,67,116,117,167,334,335,337,367,667,1667,3334,3335,3337,3367,3667,6667,16667,33334,33335,33337,33367,33667,36667,66667,166667,333334,333335,333337,333367,333667,336667,366667,666667,1666667,3333334,3333335,3333337,3333367,3333667,3336667,3366667,3666667,6666667,16666667,33333334,33333335,33333337,33333367,33333667,33336667,33366667,33666667,36666667,66666667,166666667,333333334,333333335,333333337,333333367,333333667,333336667,333366667,333666667,336666667,366666667,666666667

代码:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;

void solve(){
    int m;
    cin>>m;
    
    vector<int> a={1,2,3,4,5,6,7,12,13,15,16,17,34,35,37,38,67,116,117,167,334,335,337,367,667,
        1667,3334,3335,3337,3367,3667,6667,16667,33334,33335,33337,33367,33667,36667,66667,
        166667,333334,333335,333337,333367,333667,336667,366667,666667,1666667,3333334,3333335,
        3333337,3333367,3333667,3336667,3366667,3666667,6666667,16666667,33333334,33333335,
        33333337,33333367,33333667,33336667,33366667,33666667,36666667,66666667,166666667,333333334,
        333333335,333333337,333333367,333333667,333336667,333366667,333666667,336666667,366666667,
        666666667};

    int ans=0;
    for(auto val:a){
        if(val<=m) ans++;
    }
    cout<<ans<<endl;
}

signed main(){

	ios::sync_with_stdio(0);
	cin.tie(0);

	int t=1;
	cin>>t; 
	
	while(t--){
		solve(); 
	}

}

H. 图书馆楼梯

递推。也可看成动态规划,但通过本题无需了解任何动态规划相关知识

设:

\(f[i] [1]\) :走到楼梯第 \(i\) 层,且最后一步是迈了 1 步,的方案数

\(f[i] [2]\) :走到楼梯第 \(i\) 层,且最后一步是迈了 2 步,的方案数

如何递推得到 \(f[i] [1]\)

因为最后一步是迈了 1 步,所以 \(f[i] [1]\) 是从第 \(i-1\) 阶走来

所以\(f[i] [1]\)=\(f[i-1] [1]~+~f[i-1] [2]\)

如何递推得到 \(f[i] [2]\)

因为最后一步是迈了 2 步,且不可连续迈两步,所以 \(f[i] [2]\) 只能从 \(f[i-2] [1]\) 走来

所以 \(f[i] [2]\)=\(f[i-2] [1]\)

而递推开始时的初始值:\(f[0][1]=1\)

最后答案即为:\(f[n] [1]+f[n] [2]\)

复杂度 \(O(n)\)

代码:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int mod=1e9+7;

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

    vector<vector<int>> f(n+1,vector<int>(3,0));

    f[0][1]=1;

    for(int i=1;i<=n;i++){
        f[i][1]=f[i-1][1]+f[i-1][2];

        if(i>1) f[i][2]=f[i-2][1];

        f[i][1]%=mod;
        f[i][2]%=mod;
    }

    cout<<(f[n][1]+f[n][2])%mod;
}

signed main(){

	ios::sync_with_stdio(0);
	cin.tie(0);

	int t=1;
	// cin>>t; 
	
	while(t--){
		solve(); 
	}

}

K. 阶乘

\(n!!\) 容易计算,直接递推取模计算即可

对于 \((n!)!\),经过计算可以发现,当 \(n>12\) 时,\(n!>1e9+7\),导致 \((n!)!\) 一定是 \(1e9+7\) 的倍数 \((1*2*3*.....*(1e9+7)*.....)\)

此时 \((n!)!~mod~1e9+7\) 一定等于零

对于 \(n<=12\) 的情况,可以暴力计算

对于 \(n\) 的数值分类讨论即可

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int mod=1e9+7;

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

    vector<ll> dfact(n+1);
    dfact[0]=1;
    dfact[1]=1;

    for(int i=2;i<=n;i++){
        dfact[i]=dfact[i-2]*i;
        dfact[i]%=mod;
    }

    if(n>12){
        cout<<dfact[n];
        return;
    }

    ll factn=1;
    for(int i=2;i<=n;i++){
        factn*=i;
        factn%=mod;
    }

    ll ans=1;
    for(int i=2;i<=factn;i++){
        ans*=i;
        ans%=mod;
    }

    cout<<(ans+dfact[n])%mod;


}

signed main(){

	ios::sync_with_stdio(0);
	cin.tie(0);

	int t=1;
	// cin>>t; 
	
	while(t--){
		solve(); 
	}

}

D. 自由跳跃

分层图/拆点

分层图思想:

设从 \(1\) 号点走到 \(i\) 号点,且使用 \(j\) 次跳跃的最短路径:\(dist[i] [j]\)

按照 \(j\) 分层,对于每一层,相当于是一个朴素的最短路的模型

\(j\) 层只能从 第 \(j-1\) 层走来,所以可以计算每一层的最短路,再转移到下一层即可

拆点思想:

将原来的一个点 \(i\),拆成 \(k+1\) 个点 \(i_0\)\(i_1\) .... \(i_k\)

对于每个新点,假设 \(j\) 可以走到 \(i\),则可以建一条从 \(j_{k-1}\) 走向 \(i_k\) 的边

在新图上直接跑最短路即可 (对于本题,dijkstra 和 spfa 均可通过)

复杂度 \(O(n*k*log(nk))\) (\(n,m\) 同阶)

两种思想的代码实现完全相同

代码:

#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;
const ll inf = 1e18;
const int mod = 998244353;


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

    vector<vector<pii>> ng((k+1)*(n+1));
    vector<vector<pii>> g(n+1);

    vector<int> dist((k+1)*(n+1)+100,inf);
    dist[1]=0;

    while(m--){
        int u,v,w;
        cin>>u>>v>>w;
        g[u].push_back({v,w});
        g[v].push_back({u,w});
        ng[u].push_back({v,w});
        ng[v].push_back({u,w});
    }

    vector<vector<int>> id(n+1,vector<int>(k+1));

    for(int i=1;i<=n;i++){
        id[i][0]=i;
    }
    int now=n+1;

    for(int j=1;j<=k;j++){
        for(int i=1;i<=n;i++){
            id[i][j]=now;

            for(auto [v,_]:g[i]){
                ng[id[v][j-1]].push_back({now,0});
            }

            now++; 
        }
    }

    for(int j=1;j<=k;j++){
        for(int i=1;i<=n;i++){
            for(auto [v,w]:g[i]){
                ng[id[i][j]].push_back({id[v][j],w});
            }
        }
    }

    auto dijkstra=[&]()-> void {
        priority_queue<pii,vector<pii>,greater<pii>> q;

        q.push({0,1});
        vector<int> st((k+1)*(n+1)+100);

        while(q.size()){
            auto [d,u]=q.top();
            q.pop();

            if(st[u]) continue;
            st[u]=1;

            for(auto [v,w]:ng[u]){
                if(d+w<dist[v]){
                    dist[v]=d+w;
                    q.push({dist[v],v});
                }
            }
        }
    };

    dijkstra();

    int ans=inf;

    for(int i=0;i<=k;i++){
        ans=min(ans,dist[id[n][i]]);
    }

    cout<<ans;

}

signed main(){

	ios::sync_with_stdio(0);
	cin.tie(0);

	int t=1;
	// cin>>t; 
	
	while(t--){
		solve(); 
	}

}

I. 古文字破译 pro max

字典树维护动态规划

先将所有单词放入字典树中

\(dp [i]\) :从第 \(1\) 个字符开始,前 \(i-1\) 个字符能否全部表示出来

所以最后如果 \(dp [n+1] = 1\),则输出 Y,否则输出 N

(假设字符串下标从 \(1\) 开始,\(1-idx\)

初始状态是 \(dp [1] = 1\)

\(i\)\(1\)\(n\) ,如果 \(dp [i] = 1\),则:

枚举 \(j = [i,n]\)

我们要找到所有 \(j\),满足 \(s [i-j]\) 是一个出现过的单词

\(trie\) 树中,开始找 \(s[i-n]\),假设当前在 \(j\)

如果 \(s_j\) 存在 \(trie\) 树中,且 \(s_j\) 所在的节点不是一个单词的结尾,则继续,\(j++\)

如果 \(s_j\) 存在 \(trie\) 树中,且 \(s_j\) 所在的节点是一个单词的结尾,则 \(s[i -j]\) 可以被表示出来,则更新 \(dp[j+1]=1\), 然后继续,\(j++\)

如果 \(s_j\) 不存在 \(trie\) 树中,则 break,停止内层循环

因为单词长度最长是 100,所有内层 for 循环最多100次,复杂度可以接受

时间复杂度:\(O(T*n*max|w|)\)

一些剪枝比较优秀的非预期复杂度做法也可通过

代码:

#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;
const ll inf = 1e18;
const int mod = 998244353;

struct Trie {
    vector<array<int,26>> nxt;
    vector<bool> isEnd;
    Trie(){
        nxt.push_back(array<int,26>{});
        isEnd.push_back(false);
        for(int i=0;i<26;i++) nxt[0][i] = -1;
    }
    void insert(const string &s){
        int u = 0;
        for(char ch: s){
            int c = ch - 'a';
            if(nxt[u][c] == -1){
                nxt[u][c] = nxt.size();
                nxt.push_back(array<int,26>{});
                isEnd.push_back(false);
                for(int k=0;k<26;k++) nxt.back()[k] = -1;
            }
            u = nxt[u][c];
        }
        isEnd[u] = true;
    }
};

void solve(){
    string s;
    cin>>s;
    int n;
    cin>>n;
    Trie tr;

    for(int i=1;i<=n;i++){
        string tmp;
        cin>>tmp;
        tr.insert(tmp);
    }

    n=s.size();
    s=" "+s;

    vector<int> f(n+10);//f[i]: 1-(i-1)能被匹配上
    f[1]=1;

    for(int i=1;i<=n;i++){
        if(!f[i]) continue;
        int now=0;
        for(int j=i;j<=n;j++){
            int ch=s[j]-'a';
            if(tr.nxt[now][ch]==-1) break;
            now=tr.nxt[now][ch];
            if(tr.isEnd[now]){
                f[j+1]=1;
            }
        }
    }

    if(f[n+1]) cout<<"Y";
    else cout<<"N";
    cout<<endl;

}

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

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

    return 0; 
}

J. DATA STRUCT

本场的数据结构题,线段树、分块、珂朵莉树、平衡树均有可以通过的做法

线段树做法一:

每次修改后,区间内位置 \(i\) 的值:\(d+i-l=i+(d-l)\)

可以将其看作一个一次多项式:\(kx+b\),其中,\(k=1\)\(x=i\)\(b=d-l\)

一次操作时,区间内的 \(b\) 值是相同的,所以只需要使用线段树维护每个点的多项式和区间最大值即可

代码:

#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;
const ll inf = 1e18;
const int mod = 998244353;

class SegmentTree{
    public:
    #define lc u<<1
    #define rc u<<1|1

    struct Node{
        int l,r,mx;
        int add=inf;
    };

    int n;
    vector<int> w;
    vector<Node> tr;

    SegmentTree(int n){
        init(n);
    }

    void init(int n){
        this->n=n;
        w.resize(n+10);
        tr.resize(4*n+10);
    }

    void pushup(int u){
        tr[u].mx=max(tr[rc].mx,tr[lc].mx);
    }

    void pushdown(int u){
        if(tr[u].add!=inf){
            tr[rc].add=tr[u].add;
            tr[lc].add=tr[u].add;
            tr[rc].mx=tr[rc].r+tr[u].add;
            tr[lc].mx=tr[lc].r+tr[u].add;
            tr[u].add=inf;
        }
        return;
    }

    void build(int u,int l,int r){
        if(l==r) tr[u]={l,r,w[r],inf};
        else{
            tr[u]={l,r,0,inf};
            int mid=l+r>>1;
            build(lc,l,mid);
            build(rc,mid+1,r);
            pushup(u);
        }
    }

    int query(int u,int l,int r){
        if(l<=tr[u].l && r>=tr[u].r) return tr[u].mx;
        else{
            pushdown(u);
            int mx=-inf,mid=tr[u].l+tr[u].r>>1;
            if(l<=mid) mx=query(lc,l,r);
            if(r>mid) mx=max(mx,query(rc,l,r));
            return mx;
        }
    }

    void modify(int u,int l,int r,int k){
        if(l<=tr[u].l && r>=tr[u].r){
            tr[u].mx=tr[u].r+k;
            tr[u].add=k;
        }
        else{
            pushdown(u);
            int mid=tr[u].r+tr[u].l>>1;
            if(l<=mid) modify(lc,l,r,k);
            if(r>mid) modify(rc,l,r,k);
            pushup(u);
        }
    }
};

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

    SegmentTree tr(n);

    for(int i=1;i<=n;i++){
        cin>>tr.w[i];
    }

    tr.build(1,1,n);

    while(q--){
        int ch,l,r,d;
        cin>>ch;

        if(ch==2){
            cout<<tr.query(1,1,n)<<" ";
        }
        else{
            cin>>l>>r>>d;
            tr.modify(1,l,r,d-l);
        }
    }

}

signed main(){

	ios::sync_with_stdio(0);
	cin.tie(0);

	int t=1;
	// cin>>t;
	
	while(t--){
		solve(); 
	}

}

分块做法:

分块做法其实思想和代码都很简单

设每一块的块长是 \(\sqrt{n}\) ,则共有 \(\sqrt{n}\) 个块,每块内有 \(\sqrt{n}\) 个元素

如果一个修改区间,覆盖掉了一整个块,则直接修改这个块的标记即可

而修改区间的两边可能没有覆盖整个块,则直接暴力枚举这个块内元素即可

修改操作共两步,每步的复杂度都是 \(O(\sqrt{n})\)

查询操作直接枚举 \(\sqrt{n}\) 个块的最大值标记即可

总体复杂度 :\(O(n*\sqrt{n})\)

珂朵莉树做法:

其实题面的数据不随机,就是为了卡珂朵莉树的做法

但验题时,还是被某位来自华南理工的大佬,使用珂朵莉树这个优雅且暴力的方式通过

初始时,认为共有 \(n\) 个连续线段,当进行修改操作时,认为是将 \(l,r\) 区间推平成一个线段

使用两个多重集AB, A 维护所有线段,B 记录每个线段的最大值

修改操作时,在 A 中找到对应的区间,将其改造成一个新线段

并在 B 中删去旧线段对应的值,增加新线段对应的值

查询操作时,直接在 B 中查询最大值即可

均摊时间复杂度: \(O(n*logn*logn)\)

珂朵莉树时间复杂度严谨的数学证明:https://zhuanlan.zhihu.com/p/102786071

代码:

#include<bits/stdc++.h>
using i64 = long long;

signed main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);

    int n, q;
    std::cin >> n >> q;
    std::vector<int> a(n);
    for (auto &i : a) std::cin >> i;

    std::multiset<int> ms;

    auto cmp = [](auto a, auto b) -> auto {
        return a[1] < b[0];
    };
    std::set<std::array<int, 3>, decltype(cmp)> s(cmp); 

    for (int i = 0; i < n; i++) {
        s.insert({i + 1, i + 1, a[i]});
        ms.insert(a[i]);
    }

    while (q--) {
        int op;
        std::cin >> op;

        if (op == 1) {
            int l, r, d;
            std::cin >> l >> r >> d;

            auto it = s.find({l, r, -1});
            while (it != s.end()) {
                auto [l_, r_, v] = *it;
                if (l_ > r) break;
                it++;
                s.erase(std::prev(it));
                ms.extract(v);

                if (l_ < l) {
                    s.insert({l_, l - 1, v - (r_ - l + 1)});
                    ms.insert(v - (r_ - l + 1));
                }

                if (r_ > r) {
                    s.insert({r + 1, r_, v});
                    ms.insert(v);
                }
            }

            s.insert({l, r, d + (r - l)});
            ms.insert(d + (r - l));
        } else {
            std::cout << *ms.rbegin() << " ";
        }
    }

    return 0;
}

A. 你说得对,但是博弈论

博弈论+组合数学,来自杭电多校,本场定位防 ak 题,实际难度大致是区域赛银牌题难度

前置知识:nim 游戏,反 nim 游戏

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

称先手为A,后手为B

看成 nim 游戏时:

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

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

看成反 nim 游戏时:

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

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

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

nim 和 反 nim 的结果是确定的

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

  1. A 可以让下个房间B先手,A 可以让下个房间A先手

  2. B 可以让下个房间A先手,B 可以让下个房间B先手

  3. A 可以让下个房间B先手,B 可以让下个房间B先手

  4. A 可以让下个房间A先手,B 可以让下个房间A先手

结果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-11-19 16:07  LYET  阅读(0)  评论(0)    收藏  举报