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,且操作次数最小,所以可以想到,每次操作时,都让减少量尽可能的大。
-
如何让减少量尽可能的大?可以每次找到所有数的最大值,对这个数进行操作。
-
如何找到所有数的最大值?
每次对所有数 \(for\) 循环,以 \(O(n)\) 的复杂度找到最大值
考虑这种方法在最坏情况下的时间复杂度,假设 \(s\) 是 \(0\),则需要把每个数都一直除二除到零,此时每个数会被操作 \(log\) 次。
所以 \(n\) 个数就是操作 \(n*log\) 次,而每次操作,如果都 \(O(n)\) 的找最大值,则总复杂度是 \(O(n^2 log)\),显然会超时
考虑如何优化找最大值的过程
可以使用 \(stl\) 中的一个工具:优先队列(priority_queue)
优先队列支持的功能是:
- 可以在 \(O(1)\) 的时间内找到最小值或最大值
- 同时支持在 \(O(logn)\) 的时间内插入一个元素
- 在 \(O(logn)\) 的时间内删除最小值或最大值
- 详细的介绍: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 的结果是确定的
所以每个房间都是以下四种结果之一:
-
A 可以让下个房间B先手,A 可以让下个房间A先手
-
B 可以让下个房间A先手,B 可以让下个房间B先手
-
A 可以让下个房间B先手,B 可以让下个房间B先手
-
A 可以让下个房间A先手,B 可以让下个房间A先手
结果1:如果到这个房间,则A必胜
结果2:如果到这个房间,则B必胜
结果3:到这个房间,一定会交换下一个房间的先后手顺序
结果4:一定不会交换下一个房间的先后手顺序
总结归纳房间类型:
a:先手可以决定下一个房间的先后手
b:后手可以决定下一个房间的先后手
c:先后手交换顺序
d:先后手顺序不变
排列组合 Alice 赢的情况
一. a 或 b 不为 0 的情况
- 前面有偶数个 c,然后放一个 a,剩下的 a b 随便放(d 先不考虑,因为 d 对答案完全没影响)
- 前面有奇数个 c,然后放一个 b,剩下的 a b 随便放
- 累加这两种情况,再乘上 \(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;
}

浙公网安备 33010602011771号