Educational Codeforces Round 95 (Rated for Div. 2)A-D题解
//写于rating值1987/2184
//这场unrated…很迷的一场。
//看完A的题面,内心:“这不傻逼题么”,一看样例:“???????”。“难道是我读错题意了???”,回去又看了两遍题意,然后官方开始发公告。大概在11min的时候修改了A的样例和评测数据。
//13分钟时过掉了A,再13分钟过掉了B和C,看了D一半的题面后官方发了公告说unrated,光速赶在门禁前飞回寝室…
//D的结论以及用set和优先队列实现的思路都已经推导出来,但是set的一些函数操作并不熟悉,趁此机会学习了一波
A题
水题
题意为一开始你有1根木棒,使用1根木棒和1份燃油可以做1个火把,现在你希望做k个火把。你可以进行任意次交易,每次交易你可以选择以下两种交易方式:
1.用1根木棒交换x根木棒
2.用y根木棒交换1份燃油
现在问你最少需要几次交易可以制作出k个火把。
制作k个火把需要至少k根木棒和k份燃油。
首先我们手上是没有燃油的,且燃油只能用木棒交换获得,那么k份燃油就需要至少进行k次第二种交易用k
×
\times
×y根木棒交换获得,总共就至少需要k
×
\times
×y+k根木棒。而第一种交易方式可以看作每次交易增加x-1根木棒。因此所需第一种交易的最少次数就是(k
×
\times
×y+k)/(x-1)向上取整,再加上第二种交易需要的k次就是答案了。
#include<bits/stdc++.h>
#define ll long long
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
ll x,y,k;
cin>>x>>y>>k;
ll need=k*y+k;
ll ans=(need-1)/(x-1)+k;
if((need-1)%(x-1)) ans++;
cout<<ans<<endl;
}
}
B题
贪心
题意为给定一个长度为n的数列,有一部分位置是锁定的,我们不能改变这些位置上数字的位置,而另一部分位置是不锁定的,我们可以任意交换这些位置上数字的位置。
现在希望你对这个数列上不锁定位置上的数字重新排列,使得这个数列的前缀和数组中,负数的最大下标最小。输出任意一种构造方式。
这里前缀和数组中负数的最大下标最小,那么对于某一个固定下标的前缀和,我们肯定希望前面构造的数字都尽可能大,这个贪心对于每一个固定下标都是相同的。因此我们直接把所有不锁定位置上的数字,按照从大到小的顺序重新排一遍就可。
#include<bits/stdc++.h>
#define ll long long
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
vector<int>num;
int n;
int cas[107];
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
cin>>n;
num.clear();
num.resize(n);
deque<int>Q;//用于存储不锁定位置上的数字,排序后进行后续构造操作
for(int i=0;i<n;i++) cin>>num[i];
for(int i=0;i<n;i++)
{
cin>>cas[i];
if(!cas[i]) Q.push_back(num[i]);
}
sort(Q.begin(),Q.end());
for(int i=0;i<n;i++)
{
if(!cas[i])//对不锁定位置,我们依次按照从大到小的顺序把所有数字放进去。
{
num[i]=Q[Q.size()-1];
Q.pop_back();
}
}
for(int i=0;i<n;i++) cout<<num[i]<<' ';
cout<<endl;
}
}
C题
贪心,整体思维
题意为有n个boss依次要击杀,一部分是简单boss一部分是困难boss。你和你的朋友交替击杀boss(朋友先手),
每次可以选择击杀1个或者2个boss,但是你的朋友自己无法击杀困难boss,当他需要击杀困难boss的时候,需要使用1次道具才能击杀。现在需要你求出你和朋友最优策略的情况下,朋友使用道具的最少次数。
由于朋友先手,因此第一个boss必然由朋友击杀,如果第一个就是困难boss的话,朋友就必须要使用一次道具。之后的问题就可以看作是自己先手了。
对于连续的x个困难boss来说,如果是自己先手的话,采取贪心的策略,自己每次都击杀2个,而朋友每次都击杀1个,那么击杀完这x个boss需要的最少道具次数就是x/3个。
这n-1(除了第一个)个boss可以看成若干段连续的困难boss,他们之间存在着1个或以上的简单boss,我们是否可以通过某种策略使得每一段连续的困难boss都是由自己先手开始击杀呢?
注意到如果自己和朋友如果都只能击杀1只boss的话,就会固定为朋友只能击杀奇数下标的boss,自己只能击杀偶数下标的boss,但是现在自己和朋友可以选择击杀2只boss,一旦选择击杀2只boss,自己和朋友对应的奇偶下标就交换了。通过相邻段连续困难boss之间的简单boss,我们可以通过选择击杀2只boss交换两个人的奇偶下标而保证下一段困难boss是由自己先手开始击杀。
由此得到结论,所需最少道具次数就是除了第一个boss外的n-1个boss,每个连续困难boss的数量除以3后累加,如果第一个boss是困难就再加1即可。
#include<bits/stdc++.h>
#define ll long long
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const ll maxn=2e5+7;
ll num[maxn];
ll n;
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
cin>>n;
for(ll i=0;i<n;i++) cin>>num[i];
ll ans=0;
if(num[0]) ans++;//由于朋友是先手,因此第一个boss如果就是困难的那就必须要使用一次道具
ll temp=0;
for(ll i=1;i<n;i++)
{
while(i<n&&num[i]==1)//统计连续的困难boss有多少个
{
temp++;
i++;
}
ans+=temp/3;//除3加到答案上并置零
temp=0;
}
cout<<ans<<endl;
}
}
D题
贪心,set和优先队列维护动态个点相邻间线段长度的最大值
题意为在一条x轴上,有若干个整数点位置上有垃圾,现在你需要把这些垃圾聚集到不超过2个点的位置上。每次操作你可以选择一个下标x,把x位置上的所有垃圾移动到x-1或者x+1的位置。你需要输出最少的操作次数。并且此题有q次改变初始垃圾情况的操作,每次改变会增加或者删除一个垃圾,你需要对每次改变后输出最少的操作次数。
先推个结论,如果我们是把所有垃圾都聚集到一个点上的话,最优的操作就是垃圾的最右侧位置和最左侧位置的距离。因为我们任意在这一个线段上取一个点作为我们最终的聚集点,左侧部分从最左侧依次向右移动,右侧部分从最右侧依次向左移动,这是最优的方案,刚好等于线段长度。如果我们选择的最终聚集点在线段外的位置,自己稿纸上画一下就知道需要更多的操作次数(高中数学基础不讲了)。
现在我们可以聚集到两个点上,那么也就是说我们可以把原来的线段去除某一对相邻垃圾之间的线段,剩下两个线段分别进行聚集操作,那么我们只要使得删除的这一条线段长度最大,就是最优的方案了。
这样问题就转化为了如何维护动态个点的相邻线段长度的最大值,这里使用set和优先队列来实现。这里的思考方式在于,我们只需要关注线段中的最大值,因此可以用优先队列来实现,但是优先队列不支持任意值的删除操作,我们只需要再开一个优先队列保存待删除的线段长度即可。
而在原本的图中插入点和删除点的操作,我们只需要知道原图中新加入点左右侧是否有垃圾,且他们的位置在哪就可以知道需要删除和增加哪些线段。
具体实现见代码.
#include<bits/stdc++.h>
#define ll long long
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
int n,q;
set<ll>S;//集合S用于存储当前有哪些位置上有垃圾,用于计算实现下面两个优先队列的当前线段和待删除线段
priority_queue<ll>length_now,length_delete;
//两个优先队列,now存储当前的图上所有的相邻点之间的线段长度,delete为插入和删除点过程中产生的待删除线段
//由于我们只关注所有点的最左侧和最右侧的距离(set可计算),以及相邻点之间的线段长度中的最大值(优先队列的top()可以获得)
void insert(ll x)//增加点操作
{
if(S.size())//增加点可能会造成线段的增加和减少
{
auto now=S.lower_bound(x);//用lower_bound找到集合S中大于x的第一个位置
if(now==S.begin()) length_now.push(*now-x);//如果大于x的第一个位置就是最小的点了,那加入的点是在目前的最左侧,只增加了*now-x这一条线段
else//如果大于x的第一个位置不是最小的点
{
if(now==S.end())//如果大于x的第一个位置是集合的末尾,代表加入的点在最右侧,对now--操作变成之前最大点的位置,只增加了x-*now这一条线段
{
now--;
length_now.push(x-*now);
}
else//如果不是末尾,则代表新加入的点是在两个点的中间
{
ll temp=*now;//用temp临时记录右侧点的值,再对now--变为左侧点的位置
now--;
length_now.push(temp-x);//新增加了两条线段,分别与左右侧相邻点构成
length_now.push(x-*now);
length_delete.push(temp-*now);//原本左右侧相邻点构成的线段被删除
}
}
}
S.insert(x);
}
void erase(ll x)//同insert操作的思路
{
S.erase(x);
if(S.size())
{
auto now=S.lower_bound(x);
if(now==S.begin()) length_delete.push(*now-x);
else
{
if(now!=S.end())
{
ll temp=*now;
now--;
length_now.push(temp-*now);
length_delete.push(temp-x);
length_delete.push(x-*now);
}
else
{
now--;
length_delete.push(x-*now);
}
}
}
}
void out()
{
if(S.size()==0) cout<<0<<endl;//这里要特判一下集合S是否为空,否则下面的rbegin()-begin()的操作会出错
else
{
while(length_delete.size()&&length_delete.top()==length_now.top())//我们只关注所有线段中最大的值,因此用优先队列存储即可
{//把待删除的最大线段依次删去即可
length_delete.pop();
length_now.pop();
}
ll ans=*S.rbegin()-*S.begin();
if(length_now.size()) cout<<ans-length_now.top()<<endl;
else cout<<0<<endl;
}
}
int32_t main()
{
IOS;
cin>>n>>q;
for(ll i=0;i<n;i++)
{
ll x;
cin>>x;
insert(x);
}
out();
for(ll i=0;i<q;i++)
{
ll ope,x;
cin>>ope>>x;
if(ope) insert(x);
else erase(x);
out();
}
}