Educational Codeforces Round 93 (Rated for Div. 2)A-E题解
//写于大号rating值2075/2184,小号rating值1887/1887
//看电影鸽掉了这一场,早上补了一波题
比赛链接:https://codeforces.com/contest/1398
A题
水题
给你n条边,让你判断下是否存在三条边无法构成三角形。
无法构成三角形的情况是两条较小边的和,小于等于最大边的长度。
贪心选择所有边中最短的两条,和最长的那一条去对比下就是了,也就是让两条较小边的和尽可能的小,而让最大边的长度尽可能大。
#include<bits/stdc++.h>
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
vector<ll>num;
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
ll n;
cin>>n;
num.resize(n);
for(ll i=0;i<n;i++) cin>>num[i];
if(num[0]+num[1]<=num[n-1]) cout<<1<<' '<<2<<' '<<n<<endl;
else cout<<-1<<endl;
}
}
B题
贪心
给定一个只包含0或者1的字符串,两个人交替执行操作,每次操作可以消除一段连续的0或者连续的1。
最后两个人的分数,是各自消除的1的个数。
两个人都采取最佳策略,求先手的人最后的分数。
分析一波,
如果当前的人不取连续的1而取连续的0,那么他获得的分数是0,还将原本不连续的两片1链接在了一起帮助下一个人。显然傻子才这么干。
因此我们必然是选择连续的1,而且应该是长度最长的一段。
因此我们扫一遍字符串,看一下有几段连续的1,长度又是多少,排个序,先手后手依次当前最大的即可。
#include<bits/stdc++.h>
#define ll long long
#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--)
{
string s;
cin>>s;
vector<ll>num;//记录字符串中各段连续的1的长度
ll len=s.size();
for(ll i=0;i<len;i++)
{
if(s[i]=='1')
{
ll now=0;//当前扫到的连续1的长度
while(i<len&&s[i]=='1') now++,i++;
num.push_back(now);
}
}
sort(num.begin(),num.end());
ll ans=0;
for(ll i=(ll)num.size()-1;i>=0;i-=2) ans+=num[i];//先手从大到小取依次取第1,3,5,7...个
cout<<ans<<endl;
}
}
C题
结论,前缀和
给定一个数列,问你有多少个子连续区域,满足所有的元素和等于这个子连续区域的长度。
这个问题可以转化为前缀和来解决。
假设sum[i]代表前i个数字的总和。
如果区域[x+1,y]这一段满足要求,也就是说sum[y]-sum[x]=y-x
这个式子移项后得到sum[y]-y=sum[x]-x
也就是说实际上对于下标y来说,下标小于y的,所有满足sum[y]-y=sum[x]-x的都是满足题目要求的。
所以我们直接从头for一遍到尾,记录一下对于x<y,sum[x]-x的每个值出现了多少次,然后查询sum[y]-y这个值出现了多少次,累加到答案上即可。
#include<bits/stdc++.h>
#define ll long long
#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 n;
string s;
cin>>n>>s;
unordered_map<ll,ll>M;//M[x]=y代表,满足前i个数字的前缀和减去个数i等于x的有多少个i
M[0]=1;
ll now=0,ans=0;
for(int i=0;i<n;i++)
{
now+=s[i]-'0';
if(M.find(now-i-1)==M.end()) M[now-i-1]=1;
else
{
ans+=M[now-i-1];//最终答案加上前面已经出现过的对数
M[now-i-1]++;
}
}
cout<<ans<<endl;
}
}
D题
贪心,dp
有三种颜色的木棒,分别有R,G,B对。我们每次可以选两种不同颜色的木棒的某一对(每一对只能被选一次),构成一个长方形,问最多可以构成的最大总长方形面积。
注意到这里R,G,B的数据都比较小,只有200,所以必然会有文章。200
×
\times
× 200
×
\times
× 200不过8e6的时空复杂度。
我们可以用一个三维的dp数组dp[i][j][k],表示R选i个,G选j个,B选k个的最大总面积。
而我们在R,G,B中选i,j,k个,必然是贪心分别选择最大的i,j,k个,因此需要再排个序。
状态转移的话,dp[i][j][k],
可以是从dp[i-1][j-1][k]增加一对R和G,也就是i和j各自+1的情况转化而来,
也可以是从dp[i-1][j][k-1]增加一对R和B,也就是i和k各自+1的情况转化而来,
也可以是从dp[i][j-1][k-1]增加一对G和B,也就是j和k各自+1的情况转化而来。
#include<bits/stdc++.h>
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
bool cmp(ll a,ll b){return a>b;};
ll dp[207][207][207];
int32_t main()
{
IOS;
ll R,G,B;
cin>>R>>G>>B;
vector<ll>r(R),g(G),b(B);
for(auto &x:r) cin>>x;sort(r.begin(),r.end(),cmp);
for(auto &x:g) cin>>x;sort(g.begin(),g.end(),cmp);
for(auto &x:b) cin>>x;sort(b.begin(),b.end(),cmp);
ll ans=0;
for(ll i=0;i<=R;i++)
for(ll j=0;j<=G;j++)
for(ll k=0;k<=B;k++)
{
if(i&&j) dp[i][j][k]=dp[i-1][j-1][k]+r[i-1]*g[j-1];
if(j&&k) dp[i][j][k]=max(dp[i][j][k],dp[i][j-1][k-1]+g[j-1]*b[k-1]);
if(i&&k) dp[i][j][k]=max(dp[i][j][k],dp[i-1][j][k-1]+r[i-1]*b[k-1]);
ans=max(ans,dp[i][j][k]);
}
cout<<ans<<endl;
}
E题
贪心,离散,树状数组,前缀和
有两种法术,一种是普通法术,一种是增伤法术。
普通法术没有特殊效果,只有一个伤害值。
增伤法术除了伤害值外,还有一个增伤效果,他可以使下一个释放的法术,伤害乘2。
一开始你一个法术也不会,有n个询问,每次询问你会学会或者忘记一个法术。(保证你忘记的法术之前学过,且每个法术的伤害值不同)
现在你需要在每次询问后,输出你每个法术使用一次后,可以造成的最大伤害值。
思路是比较容易想的,但是实现比较麻烦。我用我学过的浅显的树状数组来实现了一下。
思路肯定是记录一下易伤法术的个数为sum1,然后选择当前所有法术最大的sum1个去增伤,(如果最大的sum1个全部为易伤法术,这种情况是不可能的,要用伤害最大的普通法术去替换伤害最小的易伤法术作为被增伤的法术)。
这个值我们可以通过前缀和来求。
实现难点在于,此题有n个查询,学会的法术的情况是在不断变化的。
如果用树状数组直接去记录,tree[x]表示伤害值小于等于x的法术有几个的话,注意到题目给定的法术伤害值绝对值最大为1e9,我们是没办法开这么大的数组的。
查询个数为n,我们可以先离线记录一下所有的查询,统计所有的法术伤害值,做一个离散化。这样我们就可以用树状数组来记录和查询当前的第几个大的法术伤害值是哪个了。
然后就是前缀和要如何解决了。这里我多开了一个树状数组,add函数的v本来是代表数字x出现的次数,我们把这个次数理解为伤害值,也就是一个权值,那么树状数组存储的前缀和不再是小于等于x的数出现了几次,而是下标小于等于x的法术伤害的总和。
有了实现思路后,一路实现下去就行了。
#include<bits/stdc++.h>/Users/wangzichao/Documents/wzc/wzc.xcodeproj
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const ll maxn=2e5+7;
vector<ll>origin;//离散化数组和函数
ll find(ll x)
{
return lower_bound(origin.begin(),origin.end(),x)-origin.begin();
}
//树状数组tree[x],记录离散化下标小于x的数出现了几个
ll tree[maxn],sp;
void addtree(ll x,ll v)
{
for(;x<maxn;x+=x&-x) tree[x]+=v;
}
ll sumtree(ll x)
{
ll ret=0;
for(;x>0;x-=x&-x) ret+=tree[x];
return ret;
}
void getsp()
{
ll temp=maxn-1;
while(temp)
{
temp>>=1;
sp++;
}
}
ll kth(ll k)//tree数组中第k小的数是哪个
{
ll ret=0,sum=0;
for(ll i=sp-1;i>=0;i--)
{
ret+=(1<<i);
if(ret>=maxn||sum+tree[ret]>=k) ret-=(1<<i);
else sum+=tree[ret];
}
return ret+1;
}
//树状数组treesum[x],记录离散化下标小于x的数的值的前缀和是多少
ll treesum[maxn];
void addtreesum(ll x,ll v)
{
for(;x<maxn;x+=x&-x) treesum[x]+=v;
}
ll sumtreesum(ll x)
{
ll ret=0;
for(;x>0;x-=x&-x) ret+=treesum[x];
return ret;
}
ll n;
ll ask[maxn][2];//离线记录一下n次查询的数据,等待离散化
ll ans,tot,sumtot,num1,sum1;//tot记录当前总共有几种法术,sumtot代表所有法术的伤害和,num1代表易伤法术的数量,sum1为易伤法术的伤害和
int32_t main()
{
IOS;
getsp();
ll n;
cin>>n;
for(ll i=0;i<n;i++)
{
cin>>ask[i][0]>>ask[i][1];
if(ask[i][1]>0) origin.push_back(ask[i][1]);
}
sort(origin.begin(),origin.end());
origin.erase(unique(origin.begin(),origin.end()),origin.end());//离散化
for(ll i=0;i<n;i++)
{
if(ask[i][1]>0)
{
tot++;
sumtot+=ask[i][1];
if(ask[i][0]) num1++,sum1+=ask[i][1];
ll tar=find(ask[i][1])+1;//tar为ask[i][1]在离散化数组对应的下标+1
addtree(tar,1);//tree树状数组增加的是个数
addtreesum(tar,ask[i][1]);//treesum树状数组记录的是前缀和,这里要加上权值
}
else
{
tot--;
sumtot+=ask[i][1];
if(ask[i][0]) num1--,sum1+=ask[i][1];
ll tar=find(-ask[i][1])+1;
addtree(tar,-1);
addtreesum(tar,ask[i][1]);
}
if(num1)//存在易伤法术的情况
{
if(num1==1&&tot==1) cout<<sumtot<<endl;//易伤法术只有1种,且法术总共也只有1种,那么没有法术可被增伤。
else
{
ll del=kth(tot-num1),delnow=kth(tot-num1-1);//tot-num1为没被易伤的法术数量,我们贪心选择最小的tot-num1个;
//del为当前第tot-num1小的数的下标,delnow为当前第tot-num1-11小的数的下标,用于计算下面的特殊情况
ll delsum=sumtreesum(del),delnowsum=sumtreesum(delnow);//计算最小的tot-num1以及to-num1-1个数的和为多少,
if(tot-num1-1==0) delnowsum=0;//特判一下tot-num1-1为0的时候,此时的前缀和应该为0
ll addsum=sumtot-delsum;//我们选择最大的num1个法术增伤,可以获得的增伤值
//如果增伤值等于增伤法术的伤害总和,代表我们现在增伤的num1个法术全是增伤法术,这是不可能的,我们要选择伤害最大的非增伤法术取替换伤害最小的增伤法术
if(addsum==sum1) addsum=sumtot-delnowsum-origin[kth(tot-num1+1)-1];//替换过程
cout<<sumtot+addsum<<endl;
}
}
else cout<<sumtot<<endl;
}
}