Educational Codeforces Round 96 (Rated for Div. 2)A-E题解
比赛链接:https://codeforces.com/contest/1430
A题
简单构造
题意为现在总数为n,问你能否用若干个3,5,7构成数量n,输出任意一种构造方案。
这里直接按照n%3取余后的结果分成三种情况来考虑。
当n%3=0的时候,直接构造n/3个3即可。
当n%3=1的时候,构造了n/3个3后,还会剩余1,我们最少需要匀出2个3来和这个1来构成一个7,因此此时n/3至少要有2才行。
当n%3=2的时候,构造了n/3个3后,还会剩余1,我们最少需要匀出1个3来和这个2来构成一个5,因此此时n/3至少要有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;
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
int n;
cin>>n;
int rest=n%3;
if(rest==0) cout<<n/3<<' '<<0<<' '<<0<<endl;
else if(rest==1)
{
if(n/3<2) cout<<-1<<endl;
else cout<<n/3-2<<' '<<0<<' '<<1<<endl;
}
else
{
if(n/3) cout<<n/3-1<<' '<<1<<' '<<0<<endl;
else cout<<-1<<endl;
}
}
}
B题
简单贪心
题意为有n桶水,一开始各自有一定量的水,每次操作你可以从某一个桶中倒任意数量的水到另一个桶中,问你k次操作后,最多数量和最少数量水的桶之间水的数量差最大是多少。
这里直接采取贪心的过程就可以了
1次操作后我们可以把最大的2桶水聚集在一桶中,并且得到了一个水数量为0的空桶
2次操作后我们可以把最大的3桶水聚集在一桶中,并且得到了一个水数量为0的空桶
…
所以我们直接sort之后把最大的k+1桶(注意不要超过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;
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
int n,k;
cin>>n>>k;
vector<ll>num(n);
for(auto &x:num) cin>>x;
sort(num.begin(),num.end());
k=min(k+1,n);
ll ans=0;
for(ll i=1;i<=k;i++) ans+=num[n-i];
cout<<ans<<endl;
}
}
C题
构造,贪心,小结论
题意为一开始给你一个长度为n的数列,数字为1到n。你每次操作可以挑选当前数列的两个数字,加起来除2后向上取整放回原数列,n-1次操作后令整个数列只剩下一个数字。
问这个数字最小是多少,并且构造出一种操作方案。
这里先推个小结论。
首先两个数相加除以2后得到1的情况,只能是这两个数都是1。
如果我们用一开始就有的数字1去和其他数字去构造的话,会把现在的这个1用掉并且构造出一个非1的数字,我们不可能构造出1回来。
而我们不用1,只用其他数字的话也无法构造出另一个1,无法得到两个1。
因此最后我们得到最小的值不可能是1,至少也要是2。
接着来证明最后的最小值是2。
我们直接从当前最大两个数字来构造,一开始用n和n-1得到n,之后就是n和n-2得到n-1,n-1和n-3得到n-2…一直到最后的3和1得到2。
利用这种方式,任何n的数列,都能最后构造得到2,而前面已经论证了1是不可能得到的。
由此直接输出答案2,然后每次合并最大的两个数就可以了。
#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--)
{
int n;
cin>>n;
cout<<2<<endl;
cout<<n<<' '<<n-1<<endl;
int now=n;
while(now>2)
{
cout<<now<<' '<<now-2<<endl;
now--;
}
}
}
D题
贪心,双指针
题意为给定一个长度为n的只包含0和1的字符串,每次操作你要选择当前字符串中的某一个字符删除,然后继续把剩下字符串的前缀相同的字符全部删掉。问你最多可以操作多少次。
这里直接进行贪心的过程就是了。
每次操作的第二步我们都要吧前缀相同的字符都删掉,也就是我们所有操作的第二步都会删掉一个连续的相同字符的区间,那么我们所有操作的第一步肯定就是尽可能不要减少这些区间的数量。
我们用need记录当前还有多少个第一步要删除的字符,tar指示当前的位置,对于每次操作need+1,对于当前扫到的前缀相同区间长度为L,那么其中最多L-1个是可以用于need的删除的,也就是在前面以及当前操作的时候在第一步被删除。
当剩余长度不足need,或者已经扫到末尾的时候结束循环。
#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--)
{
int len;
string s;
cin>>len>>s;
int need=0,tar=0,ans=0;//need指示当前位置后面需要删除多少个,tar为当前位置
while(1)
{
ans++;
int net=tar;
while(net+1<len&&s[net+1]==s[tar]) net++;//找相同字符的前缀最右侧下标
need++;//我们当前位置往后的地方在本次操作要再删一个数
need=max(0,need-(net-tar));//当前前缀长度-1的数字,可以在前面的操作和当前的操作中贪心删掉
if(len-net-1<=need||net==len-1) break;//剩余的部分如果比前面操作需要删除的数量多,或者已经扫到末尾了结束循环
tar=net+1;
}
cout<<ans<<endl;
}
}
E题
贪心,逆序对,冒泡模型类比
题意为给定一个只包含小写字母的字符串,你每次操作只能交换相邻两个位置的字符,问你最少需要进行多少次操作,可以使得字符串翻转过来。
首先对于同一个字母来说,翻转过后的下标都是已知的,我们执行贪心的过程,对同一个字母来说,在原字符串中的下标依次与翻转后的下标从小到大对应,就是最优的情况。
接着我们要求的就是这样的情况最少需要多少次操作,此时我们已经知道最优的方案下翻转后的字符串的每个字符,在原字符串中的下标位置。
注意到我们的操作是交换相邻两个位置的字符,那么我们完全可以把这个过程类比成冒泡排序,我们所需的最少操作次数,就是这些下标位置的逆序对数量,利用树状数组求一下就行了。
#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;
vector<int>origin[26];
int revese[maxn];//reverse[i]记录翻转后的第i个位置,在原来的字符串中是第几个位置
int n;
string s;
int tree[maxn];
void add(int x,int v)
{
for(;x<=n;x+=x&-x) tree[x]+=v;
}
int sum(int x)
{
int ret=0;
for(;x>0;x-=x&-x) ret+=tree[x];
return ret;
}
int32_t main()
{
IOS;
cin>>n>>s;
for(int i=0;i<n;i++) origin[s[i]-'a'].push_back(i);//26个字母记录从左往右的下标
for(int i=0;i<26;i++)
{
int len=origin[i].size();
for(int j=0;j<len;j++) revese[n-origin[i][len-j-1]]=origin[i][j]+1;//同一个字母,按照翻转后所在的位置,从左往右依次贪心放进去
//由于要用树状数组,所以这里让下标变成从1开始
}
ll ans=0;
for(int i=1;i<=n;i++)//利用树状数组求个逆序对,就是冒泡从原序列变成当前序列所需的最少操作次数了
{
ans+=i-1-sum(revese[i]);
add(revese[i],1);
}
cout<<ans<<endl;
}