CSP 202203 题解:未初始化警告,出行计划,计算资源调度器,通信系统管理,博弈论与石子合并
试题内容请前往CCF官网查看:
CCF-CSP计算机软件能力认证考试
http://118.190.20.162/home.page
CCF 官方题解请点击这里。
阅读本题解前,您应当了解下列知识:
- 线段树 教程
- 差分 教程
- C++ STL容器 教程
- 二叉堆 教程
这是一份以C++代码编写的CSP 专业组 202203题解。
请注意这不是CSP-S/J的中学生竞赛的题解。
由于作者并非计算机专业科班出身,水平有限,并非每一题都能完整的解答,能够提供完整解答的也不一定是最优方案,望周知。
现将模拟测试系统中的得分列举如下:
题目 | 得分 | 时间 | 内存 |
---|---|---|---|
未初始化警告 | 100 | 140ms | 2.875MB |
出行计划 | 100 | 109ms | 5.933MB |
计算资源调度器 | 100 | 31ms | 3.105MB |
通信系统管理 | 100 | 1.734s | 57.13MB |
博弈论与石子合并 | 100 | 31ms | 3.101MB |
1 未初始化警告
基础题。
遵循题面上给出的提示即可。注意“无需考虑第 j j j条赋值语句本身是否也有右值未初始化的问题”。(尽管我不太理解这一点)
时间复杂度 O ( k ) O(k) O(k)。
#include<iostream>
using namespace std;bool vis[100010];int main(){int n,k,ans=0;vis[0]=true;cin>>n>>k;for(int i=1;i<=k;i++){int x,y;cin>>x>>y;if(!vis[y]) ans++;vis[x]=true;}cout<<ans<<endl;return 0;
}
2 出行计划
基础题。这里提供两种方法。
对于每一个场所,其预计进入时间为 x x x,核酸有效期是 y y y,则持有报告时间 t ∈ [ x − y + 1 , y ] t\in[x-y+1,y] t∈[x−y+1,y]的核酸证明可以进入该场所。则对于每个场所,我们对区间 [ x − y + 1 , y ] [x-y+1,y] [x−y+1,y]加 1 1 1,表示对于该区间的所有时间点,持有该时间点的核酸报告可以进入的场所数量都加1。对于每个询问 x x x,直接查询时间 x + k x+k x+k的场所数量即可。
此即区间修改,单点查询,可以考虑使用线段树。
时间复杂度大约是 O ( m l o g N ) , N = t + k ≤ 4 × 1 0 5 O(mlogN),N=t+k \le 4\times 10^5 O(mlogN),N=t+k≤4×105。
#include<iostream>
using namespace std;struct node{int l,r,lazy,sum;
}tr[1600001];//注意要开4倍#define lc (rt<<1)
#define rc ((rt<<1)|1)
void build(int rt,int l,int r){tr[rt]={l,r,0,0};if(l==r) return;int m=(l+r)>>1;build(lc,l,m);build(rc,m+1,r);
}int pushdown(int rt){if(tr[rt].lazy!=0){tr[lc].lazy+=tr[rt].lazy;tr[lc].sum+=tr[rt].lazy*(tr[lc].r-tr[lc].l+1);tr[rc].lazy+=tr[rt].lazy;tr[rc].sum+=tr[rt].lazy*(tr[rc].r-tr[rc].l+1);tr[rt].lazy=0;}
}void add(int rt,int l,int r/*,int val*/){if(l<=tr[rt].l && tr[rt].r<=r){tr[rt].lazy++;tr[rt].sum+=(tr[rt].r-tr[rt].l+1);/*这里每次加1,一般的做法是:tr[rt].lazy+=val;tr[rt].sum+=val*(tr[rt].r-tr[rt].l+1);*/return;}pushdown(rt);int m=(tr[rt].l+tr[rt].r)>>1;if(l<=m) add(lc,l,r);if(m<r) add(rc,l,r);
}int query(int rt,int x){if(x==tr[rt].l && x==tr[rt].r) return tr[rt].sum;pushdown(rt);int m=(tr[rt].l+tr[rt].r)>>1;if(x<=m)return query(lc,x);else return query(rc,x);
}int main(){int n,m,k;ios::sync_with_stdio(false);cin>>n>>m>>k;build(1,1,400000);for(int i=0;i<n;i++){int x,y;cin>>x>>y;y=max(x-y+1,1);add(1,y,x);}while(m--){int x;cin>>x;cout<<query(1,x+k)<<endl;}return 0;
}
但是,仔细观察题意,容易发现所有的修改都在查询之前,这时候用差分时空复杂度更低,代码量也更小。
时间复杂度大约是 O ( n + N + m ) , N = t + k ≤ 4 × 1 0 5 O(n+N+m),N=t+k \le 4\times 10^5 O(n+N+m),N=t+k≤4×105。
#include<iostream>
using namespace std;int d[400010],a[400010];int main(){int n,m,k;ios::sync_with_stdio(false);cin>>n>>m>>k;for(int i=0;i<n;i++){int x,y;cin>>x>>y;y=max(x-y+1,1);//差分 d[y]++;d[x+1]--;}for(int i=1;i<=400000;i++) {a[i]=a[i-1]+d[i];}while(m--){int x;cin>>x;cout<<a[x+k]<<endl;}return 0;
}
3 计算资源调度器
中等题。
众所周知,CSP的第3题不算很难,但总是很麻烦,细节很多,调试时间会比较长。
类图
各类成员与方法简述
node 计算节点{int load : 该节点已有的计算任务数量int inArea : 该节点所在可用区int id : 该节点编号set<int> apps : 该节点上计算任务所属应用operator< : 为stl-set设计的运算符重载 (const node &x)
}
area 可用区{set<node> nodes : 该可用区上的计算节点set<int> apps : 该可用区上计算任务所属应用addLoad : 对iterator指向的节点添加appID应用的计算任务 (iterator, appID)
}
MyCloud {int n : 计算节点数量int m : 可用区数量vector<area> areas : 所有可用区不考虑计算任务反亲和性时向atArea可用区添加appID应用的计算任务:allocAnyNodeInCertainArea(int appID,int atArea)强制考虑计算任务反亲和性时向atArea可用区添加appID应用的计算任务:allocConditonalNodeInCertainArea(int appID,int atArea,int notWithApp)向atArea可用区添加appID应用的计算任务:allocNodeInCertainArea(int appID,int atArea,int notWithApp)不考虑计算任务反亲和性时向validAreasID可用区范围添加appID应用的计算任务:allocAnyNodeInRandomArea(int appID,vector<int> &validAreasID)强制考虑计算任务反亲和性时向validAreasID可用区范围添加appID应用的计算任务:allocConditonalNodeInRandomArea(int appID,vector<int> &validAreasID,int notWithApp)向validAreasID可用区范围添加appID应用的计算任务:allocNodeInRandomArea(int appID,vector<int> &validAreasID,int notWithApp,bool mustNotWithApp):处理一条输入的计算任务组:allocResOnce(int appID,int atArea,int withApp,int notWithApp,bool mustNotWithApp)
}
思路简述
每次添加计算任务时,调用allocResOnce
,检查是否指定了“计算节点亲和性”。如果指定了,则检查该可用区是否有计算节点和“计算任务亲和性”,并调用allocNodeInCertainArea
;反之,通过“计算任务亲和性”找出所有可用区,并调用allocNodeInRandomArea
。
在方法allocNodeInCertainArea
中,检查是否指定了“计算任务反亲和性”,并据此调用不考虑反亲和性的allocAnyNodeInCertainArea
或强制考虑反亲和性的allocConditionalNodeInCertianArea
。
在方法allocAnyNodeInCertainArea
中,由于我们使用set
存储节点信息并且重载了<
运算符以指定其排序规则,因此其first
元素即为计算任务最少且编号最小的计算节点,修改即可。需要注意,不能直接通过first()
指针修改,而应该裁剪出该元素,修改之再重新插入。具体代码见area::addLoad()
方法。
在方法allocConditionalNodeInCertainArea
中,由于需要考虑反亲和性,不能直接取用first
元素,需要从头遍历找到第一个满足反亲和性的计算节点,再进行修改。
allocNodeInRandomArea
,allocAnyNodeInRandomArea
和allocConditionalNodeInRandomArea
方法的思想与alloc***NodeInCertainArea
系列方法相似,只是要多一个在所有可能的可用区中再取最小节点的步骤,此处从略。
读入时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),单次查询时间复杂度不超过 O ( n l o g n ) O(nlogn) O(nlogn),总查询次数 Σ f i ≤ 2000 \Sigma f_i \le 2000 Σfi≤2000,时间复杂度 O ( n Σ f i l o g n ) O(n\Sigma f_i logn) O(nΣfilogn)。
代码
#include<bits/stdc++.h>
using namespace std;struct node{node(){}node(int a,int b,int c):load(a),inArea(b),id(c){}int load,inArea,id;set<int> apps;bool operator<(const node &x) const{if(x.load==load){return id<x.id;}else{return load<x.load;}}
};struct area{set<node> nodes;set<int> apps;set<node>::iterator ndbeg(){return nodes.begin();} set<node>::iterator ndend(){return nodes.end();}void addLoad(set<node>::iterator it,int appID){node nd=*it;nd.load++;nd.apps.insert(appID);apps.insert(appID);nodes.erase(it);nodes.insert(nd);}
};class CalcTask{public:int count,appID,atArea,withApp,notWithApp;bool mustNotwithApp;CalcTask(){cin>>count>>appID>>atArea>>withApp>>notWithApp;atArea--;int t;cin>>t;mustNotwithApp=t;}
};class MyCloud{vector<area> areas;int n,m;
public:MyCloud(){cin>>n>>m;areas.resize(m);for(int i=0;i<n;i++){int ar;cin>>ar;ar--;node t(0,ar,i+1);areas[ar].nodes.insert(t);}}int allocAnyNodeInCertainArea(int appID,int atArea){//atArea!=0(-1 actually)if(areas[atArea].nodes.size()==0) return 0;int id=areas[atArea].ndbeg()->id;areas[atArea].addLoad(areas[atArea].ndbeg(),appID);return id;}int allocConditionalNodeInCertainArea(int appID,int atArea,int notWithApp){//atArea!=0(-1 actually), notWithApp!=0if(areas[atArea].nodes.size()==0) return 0;set<node>::iterator pnd;node nd;bool nd_modified=false;for(auto ndi=areas[atArea].ndbeg();ndi!=areas[atArea].ndend();ndi++){if(ndi->apps.size()==0 || ndi->apps.find(notWithApp)==ndi->apps.end()){int id=ndi->id;areas[atArea].addLoad(ndi,appID);return id;}}return 0;}int allocNodeInCertainArea(int appID,int atArea,int notWithApp,bool mustNotWithApp) {if(notWithApp==0) return allocAnyNodeInCertainArea(appID,atArea);int ret=allocConditionalNodeInCertainArea(appID,atArea,notWithApp);if(!ret && !mustNotWithApp) ret=allocAnyNodeInCertainArea(appID,atArea);return ret;}int allocAnyNodeInRandomArea(int appID,vector<int> &validAreasID){int minNodeID=0,minAreaID=-1,minLoad=1e9;set<node>::iterator p;for(auto id:validAreasID){auto pb=areas[id].ndbeg();if(pb->load<minLoad || (pb->load==minLoad && pb->id<minNodeID)){minNodeID=pb->id;minAreaID=id;minLoad=pb->load;p=pb;}}areas[minAreaID].addLoad(p,appID);return minNodeID;}int allocConditionalNodeInRandomArea(int appID,vector<int> &validAreasID,int notWithApp){//notWithApp!=0int minNodeID=0,minAreaID=-1,minLoad=1e9;set<node>::iterator p;for(auto id:validAreasID){for(auto ndi=areas[id].nodes.begin();ndi!=areas[id].nodes.end();ndi++) {if(ndi->apps.find(notWithApp)==ndi->apps.end()){if(ndi->load<minLoad || (ndi->load==minLoad && ndi->id<minNodeID)){minNodeID=ndi->id;minAreaID=id;minLoad=ndi->load;p=ndi;}break;}}}if(minAreaID>=0) areas[minAreaID].addLoad(p,appID);return minNodeID;}int allocNodeInRandomArea(int appID,vector<int> &validAreasID,int notWithApp,bool mustNotWithApp) {if(notWithApp==0) return allocAnyNodeInRandomArea(appID,validAreasID);int ret=allocConditionalNodeInRandomArea(appID,validAreasID,notWithApp);if(!ret && !mustNotWithApp) ret=allocAnyNodeInRandomArea(appID,validAreasID);return ret;}int allocResOnce(int appID,int atArea,int withApp,int notWithApp,bool mustNotWithApp){if(atArea>=0){if(areas[atArea].nodes.size()==0) return 0;if(withApp!=0 && areas[atArea].apps.find(withApp)==areas[atArea].apps.end()){return 0;}return allocNodeInCertainArea(appID,atArea,notWithApp,mustNotWithApp);}else{vector<int> ok_areas;for(int i=0;i<m;i++){if(areas[i].nodes.size()==0) continue;if(withApp!=0 && areas[i].apps.find(withApp)==areas[i].apps.end()) continue;ok_areas.push_back(i);}if(ok_areas.size()==0) return 0;return allocNodeInRandomArea(appID,ok_areas,notWithApp,mustNotWithApp);}}void allocRes(CalcTask &tsk){for(int i=0;i<tsk.count;i++){cout<<allocResOnce(tsk.appID,tsk.atArea,tsk.withApp,tsk.notWithApp,tsk.mustNotwithApp)<<" ";}cout<<endl;}
};int main(){ios::sync_with_stdio(false);MyCloud mc;int g;cin>>g;while(g--){CalcTask tsk;mc.allocRes(tsk);}return 0;
}
4 通信系统管理
中等题。
“有效期”的处理
注意到每条申请存在“有效期”,我们可以把一条申请分为“额度增加”和“额度减少”两个操作。“额度增加”立即生效,“额度减少”到指定日期才生效。这里存在一个问题:如何知道当天会发生哪些额度减少的操作?
法一(代码中采用的方法):采用优先队列(priority_queue
,也就是堆)来解决这个问题。利用优先队列维护一个以“额度减少”操作日期为关键字的小根堆,每天开始时,比较当天日期与堆顶“额度减少”操作的日期,如果日期一致,我们使堆顶出堆并执行“额度减少”操作,直至堆顶日期大于当前日期。
法二(更好的方法):开一个长度 1 0 5 10^5 105的数组,数组元素为vector
,用于存放每天发生的“额度减少”操作。每天访问对应下标的vector
并逐一执行“额度减少”操作即可。
法一占用空间不超过 O ( A ) O(A) O(A),时间复杂度带一个log
,法二占用空间不超过 O ( m + A ) O(m+A) O(m+A),时间复杂度则没有log
。
“额度增加”与“额度减少”操作的实现
显然, 1 ≤ n , m ≤ 1 0 5 , 1 ≤ A , B ≤ 2 × 1 0 5 1\le n,m \le 10^5, 1\le A,B \le 2\times 10^5 1≤n,m≤105,1≤A,B≤2×105的范围意味着我们不能用一个 1 0 5 × 1 0 5 10^5\times 10^5 105×105二维数组来维护各机器之间的通信额度,时空复杂度均不能满足要求。
因此我们需要设计一个数据结构,满足:1. 能快速查找该机器最大额度(用于查询主要通信对象) 2. 能快速查找与特定机器的通信额度(用于实现“额度增加”与“额度减少”操作)。
对于某台机器,我们可以用map
离散化存储每台机器的通信额度。但仅用一个map
则在查询“通信主要对象”时需要遍历整个map
,速度较慢。或者我们用set
维护通信额度,其中set
的每个元素表示该机器的一个通信对象及对应额度(写一个struct
并重载运算符来实现按照额度降序排序),则我们可以 O ( 1 ) O(1) O(1)查询该机器的“通信主要对象”,但在增加和减少额度时必须遍历set
才能确定某个通信对象的既有额度。笔者出于偷懒考虑,同时维护了上述map
和set
,以达到“博采众长”的目的。具体实现请见代码。
“通信孤岛”
关于通信孤岛,我们容易知道:
- 一台机器额度增加时,它一定不再是通信孤岛;
- 一台机器只有在额度减少时,才有可能变成通信孤岛;
因此,我们在执行“额度增加”与“额度减少”操作时,顺便检查该机器是否时“通信孤岛”即可。具体而言,维护一个通信孤岛集合set
,初始时集合中包括所有机器,“额度增加”时erase
对应机器,“额度减少”时检查额度是否下降至 0 0 0,并insert
到该集合中,查询通信孤岛数量时,调用size()
方法即可。
“通信对”
我们记 p a i r ( x ) pair(x) pair(x)为 x x x的所在“通信对”的另一台机器。每次额度增减操作涉及 u , v u,v u,v两台机器,考虑通信对,则至多涉及 { u , v , p a i r ( u ) , p a i r ( v ) } \{u,v,pair(u),pair(v)\} {u,v,pair(u),pair(v)}这些机器。
维护一个通信对集合set<pair<int,int> >
,保证pair.first
小于pair.second
,防止重复。每次额度增减操作前,先删除 u , v u,v u,v机器对应的通信对(如果有),操作后再计算新的通信对,如果有则加入即可。查询通信对数量时,调用size()
方法即可。
其它细节
- 定义每台机器的“通信主要对象”为当前时刻与该机器的每日可用额度最大的机器(如果有并列,则取其中编号最小的机器);
- 由于不同申请的效果是可以叠加的,且单次申请额度 1 ≤ x ≤ 1 0 9 1\le x \le 10^9 1≤x≤109,总共申请次数 A ≤ 2 × 1 0 5 A\le 2 \times 10^5 A≤2×105,因此额度最大可能到 2 × 1 0 14 2\times 10^{14} 2×1014,请务必开
long long
;
笔者由于不注意审题,上述问题各错一次。交了3次才成功。
代码
#include<bits/stdc++.h>
using namespace std;typedef long long ll;struct _itm{ll flow;int id;//表示到id号机器的流量额度为flow//运算符重载以指定set排序方法bool operator<(const _itm &m)const{if(flow==m.flow){return id<m.id;}else{return flow>m.flow;}}
};class data_struct{map<int,ll> mp; //mp[id]=flowset<_itm> st;void __st_modif(_itm item,int flow_mod){//调整到某机器的额度auto i=st.find(item);st.erase(i);if(item.flow+flow_mod>0) st.insert({item.flow+(ll)flow_mod,item.id});}
public:int get_main(){//查询通信主要对象if(st.size()>0) return st.begin()->id;else return 0;}void add(int v,int x){//当前机器增加到机器v的额度xauto i=mp.find(v);if(i==mp.end()){mp[v]=x;st.insert({x,v});}else{__st_modif({i->second,i->first},x);i->second+=(ll)x;}}void del(int v,int x){//当前机器减少到机器v的额度xauto i=mp.find(v);__st_modif({i->second,i->first},-x);i->second-=(ll)x;if(i->second<=0) mp.erase(i);}bool is_island(){//判断是否为通信孤岛return st.size()==0;}
}ds[100001];struct del_info{//额度减少的信息结构体int u,v,x,atday;bool operator<(const del_info& d)const{return atday>d.atday;}
}; priority_queue<del_info> del_list;
set<int> islands;
set<pair<int,int> > pairs;pair<int,int> try_gen_pair(int u){int um=ds[u].get_main();if(ds[um].get_main()==u) return {min(u,um),max(u,um)};else return {0,0};
}inline void add_once(int u,int v,int x){ds[u].add(v,x);//增加额度islands.erase(u);//从通信孤岛集合中删除
}inline void add(int u,int v,int x,int y,int day){//增加u,v额度x//删除通信对auto pu=try_gen_pair(u),pv=try_gen_pair(v);pairs.erase(pu);pairs.erase(pv);//执行增加操作add_once(u,v,x);add_once(v,u,x);//加入额度减少优先队列del_list.push({u,v,x,day+y});//检查并加入通信对auto pu2=try_gen_pair(u),pv2=try_gen_pair(v);if(pu2.first>0) pairs.insert(pu2);if(pv2.first>0) pairs.insert(pv2);
}inline void del_once(int u,int v,int x){ds[u].del(v,x);//减少额度if(ds[u].is_island()) islands.insert(u);//判断是否变为通信孤岛
}inline void del(int u,int v,int x){//减少u,v额度x//删除通信对auto pu=try_gen_pair(u),pv=try_gen_pair(v);pairs.erase(pu);pairs.erase(pv);//执行减少操作del_once(u,v,x);del_once(v,u,x);//检查并加入通信对auto pu2=try_gen_pair(u),pv2=try_gen_pair(v);if(pu2.first>0) pairs.insert(pu2);if(pv2.first>0) pairs.insert(pv2);
}int del_expired(int day){//减少过期额度while(!del_list.empty()){auto tp=del_list.top();if(tp.atday<=day) {del_list.pop();del(tp.u,tp.v,tp.x);}else{break;}}
}int main(){int n,m,u,v,x,y,k,p,q,l;ios::sync_with_stdio(false);cin>>n>>m;for(int i=1;i<=n;i++) islands.insert(i);for(int i=1;i<=m;i++){cin>>k;del_expired(i);for(int j=0;j<k;j++){cin>>u>>v>>x>>y;add(u,v,x,y,i);}cin>>l;for(int j=0;j<l;j++){cin>>x;cout<<ds[x].get_main()<<endl;}cin>>p>>q;if(p)cout<<islands.size()<<endl;if(q)cout<<pairs.size()<<endl;}return 0;
}
5 博弈论与石子合并
中等题。参考了这篇题解,在此表示感谢。
我们从直觉上可以体会到:小Z只需合并石子而无需丢弃石子,小C只需丢弃石子而无需合并石子。
小Z想让石子最多,那么他肯定不会丢石子。你可能会想,如果小C先把边上的石子合并起来,再一次性丢掉,会不会是更优解呢?实际上效果是一样的,譬如我们先合并最左边的两堆石子,再丢掉(2次操作)和连续2次丢掉最左边的石子(也是2次操作)基本是等价的,因此只要丢石子就可以了。
(实际上这里应该提供严谨的证明,但是由于笔者数学水平有限,不会证,只能提供一些直观的解释。总之,上述粗体字是正确合理的结论。)
我们采用数学归纳法,并以小Z先手为例来讨论问题。
- 设当前有2堆石子,小Z直接合并即可;
- 设当前有4堆石子, a 1 , a 2 , a 3 , a 4 a_1,a_2,a_3,a_4 a1,a2,a3,a4,小Z合并两堆石子后还剩3堆,小C一定扔不掉中间那一堆,因此小Z合并 a 2 a_2 a2和 a 3 a_3 a3一定不会被扔掉,最后小Z可以得到 a 2 + a 3 + m i n { a 1 , a 4 } a_2+a_3+min\{a_1,a_4\} a2+a3+min{a1,a4},另外两种可能分别是 a 2 + m i n { a 1 , a 3 + a 4 } a_2+min\{a_1,a_3+a_4\} a2+min{a1,a3+a4}以及 a 3 + m i n { a 1 + a 2 , a 4 } a_3+min\{a_1+a_2,a_4\} a3+min{a1+a2,a4},显然 a 2 + a 3 + m i n { a 1 , a 4 } a_2+a_3+min\{a_1,a_4\} a2+a3+min{a1,a4}是最大的。因此小Z一定会从中间合并。
- 设当前有3堆石子, a 1 , a 2 , a 3 a_1,a_2,a_3 a1,a2,a3。如果 a 1 > a 3 a_1>a_3 a1>a3,
a) 小Z合并后得到 a 1 + a 2 , a 3 a_1+a_2,a_3 a1+a2,a3,显然 a 1 + a 2 > a 3 a_1+a_2>a_3 a1+a2>a3,小C扔掉前一堆,剩 a 3 a_3 a3;
b) 小Z合并后得到 a 1 , a 2 + a 3 a_1,a_2+a_3 a1,a2+a3,然后小C扔掉其中大的那一堆,剩 m i n { a 1 , a 2 + a 3 } min\{a_1,a_2+a_3\} min{a1,a2+a3}。但由于 a 1 > a 3 , a 2 + a 3 > a 3 a_1>a_3,a_2+a_3>a_3 a1>a3,a2+a3>a3,剩下的石子一定比a)情况多;
因此应该合并 a 2 , a 3 a_2,a_3 a2,a3;同理不难发现, a 1 < a 3 a_1<a_3 a1<a3时,应该合并 a 1 , a 2 a_1,a_2 a1,a2; a 1 = a 3 a_1=a_3 a1=a3时,两种合并方式没有差异。 - 设当前有 2 k ( k > 2 ) 2k (k>2) 2k(k>2)堆石子,每堆数量依次记作 a 1 , a 2 , . . . , a k , a k + 1 , . . . , a 2 k a_1,a_2,...,a_k,a_{k+1},...,a_{2k} a1,a2,...,ak,ak+1,...,a2k,小Z合并中间的石子意味着合并了 a k a_k ak与 a k + 1 a_{k+1} ak+1,此时还剩 a 1 , a 2 , . . . , a k + a k + 1 , . . . , a 2 k a_1,a_2,...,a_k+a_{k+1},...,a_{2k} a1,a2,...,ak+ak+1,...,a2k这 ( 2 k − 1 ) (2k-1) (2k−1)堆石子,显然小C会扔掉 a 1 a_1 a1和 a 2 k a_{2k} a2k中更多的那堆石子。此时剩下 ( 2 k − 2 ) (2k-2) (2k−2)堆石子。这种情况最终会归纳到情况1。
实际上,在这种情况下,最后留下的那堆石子一定是长度为 ( k + 1 ) (k+1) (k+1)的一段石子数量之和,我们只要求
m i n i = 1 , 2 , . . . , k { ∑ j = i i + k a j } \underset{i=1,2,...,k}{min}\left\{\ \sum_{j=i}^{i+k} a_j\right\} i=1,2,...,kmin{ j=i∑i+kaj}
时间复杂度 O ( n ) O(n) O(n)。 - 设当前有 ( 2 k + 1 ) ( k > 1 ) (2k+1) (k>1) (2k+1)(k>1)堆石子,每堆数量依次记作 a 1 , a 2 , . . . , a k , a k + 1 , a k + 2 , . . . , a 2 k , a 2 k + 1 a_1,a_2,...,a_k,a_{k+1},a_{k+2},...,a_{2k},a_{2k+1} a1,a2,...,ak,ak+1,ak+2,...,a2k,a2k+1,由于该情况最终归纳于情况2,因此小Z无论合并哪一堆石子,总是有可能被小C扔掉。我们调整一下操作顺序,令小Z先连续合并 k k k次石子,再让小C连续扔掉 k k k次石子,最后剩下1堆石子。小Z连续 k k k次合并相当于把数列切成 ( k + 1 ) (k+1) (k+1)段,分段求和,小C会连续扔掉 k k k次石子即求这 ( k + 1 ) (k+1) (k+1)个和中的最小值,而小Z希望这个最小值最大。
最小值最大和最大值最小问题通常使用二分答案的方法来解决。应用“二分答案”的充分条件是:
(1) 所求答案 A ∈ [ L , R ] A\in[L,R] A∈[L,R];
(2) 任意 x ∈ [ L , A ] x\in[L,A] x∈[L,A]均为合法值而任意 x ∈ ( A , R ] x\in(A,R] x∈(A,R]均为非法值,或任意 x ∈ [ L , A ) x\in[L,A) x∈[L,A)均为非法值而任意 x ∈ [ A , R ] x\in[A,R] x∈[A,R]均为合法值;
(3) 存在某种算法判断任意 x ∈ [ L , R ] x\in[L,R] x∈[L,R]是否为合法值;
在这个问题中,我们记所求答案——即最后剩下的石子数——为 A A A,显然 A ∈ [ m i n { a 1 , a 2 , . . . , a n } , a 1 + a 2 + . . . + a n ] A\in[min\{a_1,a_2,...,a_n\},a_1+a_2+...+a_n] A∈[min{a1,a2,...,an},a1+a2+...+an],简便起见也可以认为答案范围是 [ 1 , 1 0 9 ] [1,10^9] [1,109]。
我们称一个值 x x x是“合法”的,当且仅当答案为 x x x时,这 ( 2 k + 1 ) (2k+1) (2k+1)段石子一定可以被分割为连续的 d d d段,满足 d > k d>k d>k且每一段的和都大于等于 x x x;
通过上述范围和合法性判断算法,我们可以很容易的设计出情形4的解答,详见代码。
总的时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)。
细心的读者可能会想到:“合法”的值,就一定可以通过小C和小Z的游戏得到吗?(假设两人未必是绝顶聪明的)不一定。显然,一个“合法”的 x x x,只有在满足**至少有一段的和为 x x x**的情况下,才是可能通过游戏得到的。我们称满足上述条件的 x x x为潜在答案。
假设我们找到了一个合法值 x x x,但是它不是潜在答案(取不到等号),那么我们记每一段和的最小值为 x ′ x' x′,显然有 x ′ > x x'>x x′>x,而且我们容易发现 x ′ x' x′也必然是合法值。因此,最大的“合法”值一定是一个“潜在答案”。
小C先手时,必定会扔掉左右两堆中更大的那一堆,然后转化到小Z先手的情况。
#include<bits/stdc++.h>
using namespace std;int a[100000],n;int add_mid(){int len=n/2+1;int s=0,mins;for(int i=0;i<len;i++){s+=a[i];}mins=s;for(int i=len;i<n;i++){s+=(a[i]-a[i-len]);mins=min(mins,s);}return mins;
}bool check(int x){//答案合法性检查器 int s=0,cnt=0;for(int i=0;i<n;i++){s+=a[i];if(s>=x) {s=0;cnt++;}}return cnt>n/2;
}int bi_ans(int l,int r) { //标准二分答案模板 int ans=0;while(l<=r){int m=(l+r)/2;if(check(m)) {ans=m;l=m+1;}else{r=m-1;}}return ans;
}int main() {int k,minx=1e9,sum=0;ios::sync_with_stdio(false);cin>>n>>k;for(int i=0;i<n;i++){cin>>a[i];minx=min(a[i],minx);sum+=a[i];}if((n+(1-k))%2){//小Z先手,n为奇数 cout<<bi_ans(minx,sum);}else{//小Z先手,n为偶数cout<<add_mid();}return 0;
}
CSP 202203 题解:未初始化警告,出行计划,计算资源调度器,通信系统管理,博弈论与石子合并相关推荐
- CSP认证 202203-1 未初始化警告
题目传送门 题目背景 一个未经初始化的变量,里面存储的值可能是任意的.因此直接使用未初始化的变量,比如将其赋值给另一个变量,并不符合一般的编程逻辑.代码中出现这种情况,往往是因为遗漏了初始化语句.或是 ...
- ~2 ccf 2022-03-1 未初始化警告
未初始化警告 题目描述 输入 输出 样例输入 样例输出 源代码 子任务 关于这题 题目描述 输入 输出 样例输入 10 7 1 2 3 3 3 0 3 3 6 2 2 1 8 2 样例输出 3 源代码 ...
- 4454. 未初始化警告(只是题的长别怕,详细解读)
题目 一个未经初始化的变量,里面存储的值可能是任意的. 因此直接使用未初始化的变量,比如将其赋值给另一个变量,并不符合一般的编程逻辑. 代码中出现这种情况,往往是因为遗漏了初始化语句.或是打错了变量名 ...
- ccf-csp历年满分题解 + 总结汇总(已完成55题,持续更新中...)
2013-12 出现次数最多的数 ISBN号码 最大的矩形 有趣的数 I'm stuck! 2014-03 相反数 窗口 命令行选项 ...
- CSP 202203-2出行计划
问题分析: 差分板子.根据题意描述可以反推得到对于每个出行计划合法的核酸检测的时间区间.假设在x时刻进入公共场所,其需要c小时内的核酸证明,k小时候出核酸结果可以推出这个合法区间为[x−k−c+1,x ...
- CSP认证 202203-2 出行计划(多种解法)
解法一前置知识:差分数组 解法二前置知识:线段树 试题编号: 202203-2 试题名称: 出行计划 时间限制: 1.5s 内存限制: 512.0MB 问题描述: 问题描述 最近西西艾弗岛上出入各个场 ...
- C ++标准是否允许未初始化的bool使程序崩溃?
本文翻译自:Does the C++ standard allow for an uninitialized bool to crash a program? I know that an " ...
- 2019奥迪创新实验室大赛招募第一弹|趁现在,定一份未来出行计划
2019奥迪创新实验室大赛招募第一弹|趁现在,定一份未来出行计划 人们对于出行的想象,从未停止. 1899年,欧洲的一本杂志曾组织一群美术家.插画家,做了关于100年后世界的想象,并把他们想到的样子画 ...
- C语言中变量未初始化的处理
实例测试1--DEV-C++环境 : #include <stdio.h> //全局整形变量 int int_var1; static int stc_int_var1; short in ...
最新文章
- Java中的进程与线程
- SpringBoot 中 @RequestBody的正确使用方法
- 机器学习(三十八)——博弈论(1)
- 关于什么时候用pop什么时候用dismiss
- 欧几里德算法求最大公约数
- 抓到一只苍蝇 writeup
- 离开小厂进大厂的第一周,BTAJ大厂最新面试题汇集,面试总结
- java canvas 缩放图片_java-GWT:放大和缩小画布绘图
- php 小程序 运动步数_微信小程序步数运动-收益讲解
- 如何防范电改下分布式光伏的风险
- 一次领光天猫双 11 所有优惠卷
- Java从入门到精通 第11章 枚举
- 国科大学习资料--自然语言处理(宗成庆)-2016期末考试题
- MD5加密算法简单实现
- python词云背景图_python数据分析可视化,制作你想要的背景图词云
- 岁月如水-指间流逝-难觅难留
- 解决浏览器突然上不了网的问题
- java 斗地主洗牌发牌
- python内置库turtle绘制有趣图形
- 路由器pppoe拨号服务器无响应,pppoe服务器无响应
热门文章
- 加盐hash保存密码的正确方式
- day12. 一. 系统模块
- 能动口“布”动手 OPPO小布打造便捷高效智能生活
- 74. 学会使用 SAP ABAP Application Log 在代码里添加应用日志记录功能
- 碱性干电池的内阻测试方法_【干货】关于电池的内阻知道这几点就够了?
- java jsp 仓库存储管理系统的设计zcd900
- spring5高级编程_我也来推荐一波你看了以后大呼过瘾的编程书
- 【C语言篇】初识C语言
- 案例分享:公园智慧路灯系统解决方案详细解析
- 在Windows系统中 开机自动运行一个Python脚本 或者自动运行一个软件