Codeforces Round #666 (Div. 2)A-E题解(DE已施工完毕)

Codeforces Round #666 (Div. 2)A-E题解
//写于rating值2033/2184
//本场打的div1,本场出了AB题,rating值-42
//尽管存在读错题意白给4发的原因,但是如果没办法稳定出div2的E也就是div1的C的话,是没办法稳定在黄名或者打到更高的rating值的,毕竟也不是每次都是状态极佳可以A-D无失误手速极快。
//E题是看div1榜单排名从上往下第二位中国选手的代码理解的,昨晚的时候思路已经非常接近这个解法了,但是缺了一个关键性的结论和操作实现方法,我现在的水平和思维力还差了一根弦。斗胆也写一下E的题解。另外注意下B题是不需要真写个三分出来的
//掉分不重要,学到新东西是最重要的,以后一定会打到更高的分数走到更远的地方。

比赛链接:https://codeforces.com/contest/1397
A题
水题…

题意为给你n个字符串,你可以任意改变其中字符所在的位置,还可以把某个字符移到任意一个字符串里。问你能否在一定次数的操作后,让这n个字符串相等(字典序的相等也就是字符串相同)。

由于我们可以任意改变每一个字符的位置和所在字符串,并且不限制操作次数,因此字符原本的所在位置是没有意义的。我们只需要关注每个字符出现了几次即可。
又由于我们的目标是让这n个字符串相同,那么相同下标位置上的字符必然相同,每个下标上字符对应的个数为n,由这个条件容易推得,如果我们能使得n个字符串相同,我们的每个字符的出现次数,必定是整除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;

int num[26];

int32_t main()
{
    IOS;
    int t;
    cin>>t;
    while(t--)
    {
        int n;
        cin>>n;
        memset(num,0,sizeof(num));
        for(int i=0;i<n;i++)
        {
            string s;
            cin>>s;
            for(int j=0;j<s.size();j++)
                num[s[j]-'a']++;
        }
        bool flag=1;
        for(int i=0;i<26;i++)
            if(num[i]%n) flag=0;
        if(flag) cout<<"YES"<<endl;
        else cout<<"NO"<<endl;
    }
}

B题
三分(并不需要真的写一个三分)
数据分析,暴力

给定一个数列,要求你对它进行重新排列顺序后,再做若干次的增加1或者减少1某一个位置上数字的大小的操作,使得整个数列是一个等比数列,且比值为正数。
现在要你求出最小的操作次数。

第一步是贪心的过程,很明显的我们应当先把原来的数列按照从小到大排序好,理由是等比数列是不断增大的,我们按照贪心的思路要让小的和小的匹配,大的和大的匹配,这样的差值和最小。
第二步是要认识到,对于我们需要的操作次数ans来说,我们选择的等比数列比值定为x,那么函数ans=f(x)是一个单峰函数,是一个凹函数,我们可以通过三分来求极小值。

到这步之后,真的去写一个三分也是可行的,注意一下数据范围,特判一下等比数列的值爆longlong上限,也是可以的。

但是实际上并不需要真的写一个三分算法出来,在思考三分算法的实现过程中,我们会意识到,等比数列的增长速度是极其迅速的,当比值x稍微稍微大一点点的时候,就会在极少的项数后便超出longlong的上限。

我们分析,n的范围是大于等于3小于等于1e5,longlong的上限为1e19级别,而数列中的最大值只有1e9。
这里需要有一种对于数值的敏感性,注意到题目给的第二组样例,输入n=3,三个数字都为最大的1e9,此时我们需要的操作数才1e9的级别,代表此时的等比数列比值大小是很小的。撑死了也就是1e3的级别。

同时我们要认识到,n为最小值3,三个数字都为最大的1e9时,此时的比值是最大的。
此处证明的话,当长度相同的时候,我们希望答案等比数列比值更大的话,必定是要数列中值更大,因此我们全部取1e9。
当长度不同,由上一条结论我们已经知道了,为了让最后比值更大,数列中每一项都是1e9。而很明显的,对于n=3的答案x来说,n>3的其他数列,比x更大的比值,得到的ans必然要大于比值为x时。此处自己稿纸上想一下吧。

经过以上结论后,我们知道最后的最优比值最大也只是一个1e3左右的值。我们可以暴力从1开始枚举比值,记录比值为i-1的所需的操作次数,与比值为i时候的所需操作次数比较,如果比值i时更小或相等,代表我们刚好位于凹函数的谷底,比值为i-1时即为最小操作次数。此时break即可。
同时我们要注意一下,如果最后答案的比值对应的等比数列最后一项已经很接近longlong上限,下一项会爆掉longlong,需要加个特判break。

#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;
vector<ll>num;

int32_t main()
{
    IOS;
    cin>>n;
    num.resize(n);
    for(auto &x:num) cin>>x;
    sort(num.begin(),num.end());
    ll ans=llINF;
    for(ll i=1;i<=1e5;i++)
    {
        ll temp=0,now=1;//temp记录比值为i时候所需的操作次数
        for(ll j=0;j<n;j++)
        {
            temp+=abs(now-num[j]);
            if(now>llINF/i) {temp=-1;break;}//如果等比数列当前项超过了longlong上限则结束循环
            now*=i;
        }
        if(temp==-1) break;//temp-1代表等比数列某一项已经超出longlong上限,结束循环
        if(temp<ans) ans=temp;
        else break;//temp>=ans代表我们已经到达凹函数的“谷底”
    }
    cout<<ans<<endl;
}

C题
构造

题意为给定一个数列a包含n个整数(正负零),你需要在进行恰好三次操作后,使得数列a中的每个数都为零。
每次操作,你可以选择这个数列中一段连续的下标
,连续下标区域长度为x,你可以对这段下标区域的每个数字,减去x的任意倍数(不同下标的数可以减去x不同的倍数)。
现在需要你输出三次操作的操作方案。

有一个简单且普遍使用的构造方法:
我们第一次选择长度n-1的区域,对a[i]加上a[i] × \times × (n-1)。
第二次选择长度n的整段数列,对a[i](注意这里的a[i]指的是初始的a[i]值,不是第一次操作后的值)减去a[i] × \times ×
n。
此时两次操作的公共部分的n-1个数字,值会变为a[i]+a[i] × \times ×(n-1)-a[i] × \times ×n=0,已经被清零。
此时还会剩下一个数字不一定为0,我们在第三次操作直接选择这个数字所在的单个长度区域,减去这个数的值即可。

另外注意特判长度n=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;

ll n;
vector<ll>num;
vector<ll>cas;

int32_t main()
{
    IOS;
    cin>>n;
    num.resize(n);cas.resize(n);
    for(auto &x:num) cin>>x;
    if(n==1)
    {
        cout<<1<<' '<<1<<endl;
        cout<<1<<endl;
        cout<<1<<' '<<1<<endl;
        cout<<-1<<endl;
        cout<<1<<' '<<1<<endl;
        cout<<-num[0]<<endl;
    }
    else
    {
        cout<<2<<' '<<n<<endl;
        for(ll i=1;i<n;i++)
        {
            if(i!=1) cout<<' ';
            cout<<(n-1)*num[i];
        }
        cout<<endl;
        
        cout<<1<<' '<<n<<endl;
        for(ll i=0;i<n;i++)
        {
            if(i) cout<<' ';
            cout<<n*num[i]*-1;
        }
        cout<<endl;
        
        num[0]-=n*num[0];
        cout<<1<<' '<<1<<endl;
        cout<<-num[0]<<endl;
    }
}

D题
博弈

题意为有n堆石头,两个人轮流从中选某一堆,从中拿出一个石头,当前拿的人不能选择上一个人刚选过的那一堆石头。谁无法拿出石头,谁就输。
现在给你这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<ll>num;

int32_t main()
{
    IOS;
    int t;
    cin>>t;
    while(t--)
    {
        int n;
        cin>>n;
        num.clear();
        num.resize(n);
        ll sum=0;
        for(int i=0;i<n;i++)
        {
            cin>>num[i];
            sum+=num[i];
        }
        sort(num.begin(),num.end());
        if(n==1) cout<<"T"<<endl;
        else if(n==2)
        {
            if(num[1]>num[0]) cout<<"T"<<endl;
            else cout<<"HL"<<endl;
        }
        else
        {
            if(num[n-1]*2>sum) cout<<"T"<<endl;
            else
            {
                if(sum&1) cout<<"T"<<endl;
                else cout<<"HL"<<endl;
            }
        }
    }
}

E题
贪心,结论,dp

题意
你在玩一个射击闯关的游戏,你拥有三种枪,这三种枪的性能如下:
第一种枪:装载子弹消耗r1的时间,装载完毕后可以减去一只怪物的1点血量。
第二种枪:装载子弹消耗r2的时间,装载完毕后可以减去当前所在层所有怪物1点血量。
第三种枪:装载子弹消耗r3的时间,装载完毕后可以直接杀死某一只怪物。
(r1<=r2<=r3)

现在有n层地牢,每层地牢有num[i]只普通怪物,普通怪物只有一点血量,每层除了普通怪物外还有一只boss,boss拥有两点血量。在该层的所有普通怪物被杀死之前,第一种枪和第三种枪无法攻击boss,但是第二种枪可以直接杀死所有普通怪物并伤害boss1点血量。

你可以在任意时刻移动到相邻层的地牢,另外有一个特殊的规则,如果你对boss造成了伤害但没有杀死boss,就必须立刻移动到相邻层的地牢去。你每次在相邻地牢移动,需要消耗d的时间。

对于任意一种枪,你都不能在在地牢之间移动时进行装载子弹的操作,你只能在到达某一层地牢后才能装载子弹,并且假定你射击子弹攻击怪物的时间消耗为0。

现在你从第1层地牢出发,目标是杀死所有层地牢里的boss,需要你求出最小的时间消耗。

解题思路
杀死每层boss的方案可以按照第一次是否直接杀死分为两种,
一种是直接杀死该层boss,装载子弹的时间消耗为r1 × \times ×num[i]+r3。
另一种是第一次对boss造成一点伤害后离开该层,在之后再次回到该层杀死boss,装载子弹的时间消耗为min(r2+r1,r1 × \times ×num[i]+r1$\times$2)。

其中第一种杀死boss的方法我们只要经过目标层一次即可,但是第二种方法我们必须经过目标层至少一次。

容易想到,我们可以从第一层一路走到第n层,再折返回来走回第1层,这样的话除了第n层外的每一层我们都经过了两次,每一层都可以选择两种杀死boss方法中更优的那一种。但是很明显的,这种方案不一定是最优的。

因为我们在某些层数可以选择从3层到4层,再从4到3,在3到4,在4到5…这样折返的方案,因此上一段并不一定是最优的。
但是我们这里要从上一段的方案中推出一个贪心结论,如果我们的路径中有一段从第n层回头到第x层,那么从x层到第n层这一段,我们必然只有从x层一路走到第n层再回头到第x层这一种最优方案。理由如下图
在这里插入图片描述
图中的箭头代表一次相邻层数之间的移动,箭头的头部代表移动到该层一次。注意到红色圈出来的部分,第x层到n-1层都已经有两个箭头指向自己,代表经过了这一层两次,已经可以选择最优的杀死boss方案。而如果我们在一个区域中增加移动操作,如紫色圈出的在x+1层和x+2层之间移动,这个移动操作是无效操作,因为x+1层和x+2层杀死boss已经是最优的方案了,无法再被优化。
当然你会注意到此时第n层只经过了一次,我们只能选择直接杀死boss的方案,如果在n-1层和n-2层增加一个来回的话,是有可能优化的,但是这一点我们先不考虑,这一点会在后面的dp过程中被考虑到。

接着从样例可以看出,最优的方案中,我们最后所在的位置不一定是最后一层也就是第n层。也就是我们最后所在的终点可能是任何一层。
那么我们可以按照最后所在的层数分为n种方案进行考虑,如果我们最后所在位置是第x层,由上面的推导可以得知,第x层右侧的部分最优方案就是x层一路走到n层n层一路走回到n层。那么此时左侧的最优方案又应该如何得到?
由于我们最后是要在第x层,那么在x左侧的层数如果有走回头路的情况,x=5的时候,如果从3走到4,又从4走回头路走到3的话,必须要重新从3走到4,否则无法到达5层。如下图:在这里插入图片描述
红色圈出的是我们x层左侧的基本方案,是我们必须走的部分,如果在这个部分要添加回头方向的话,就必须要再补上对应的相反方向才能走到x层,也就是我们要优化这部分杀死boss的时间消耗的话,我们在不同地牢间的移动,在基础方案上,每次增加移动都必须是i到i+1和i+1到i两种移动作为一对,同时添加进去。

接着我们考虑,在x层左侧的部分,上述的添加相邻层数之间的移动,应该如果选择。这里很明显要采用dp来求解,但是如何使用dp来正确求解是一个难思考的点。
在稿纸上画图找规律如下:
在这里插入图片描述
注意到上面图片中,添加一组移动进去的方案,会使得i-1层和i层变为经过2次,是可能产生时间优化的。
添加相邻两组进去的方案,注意到第i层经过了3次,但是旁边的i-1层和i+1层都是经过2次,这种方案仍然是可能产生优化的。
但是对于添加相邻三组进去的方案,我们注意红色圈出的第i-1层和第i层都经过了3次,这两层之间的添加部分是可以被删去的。
从上述可以结论出,我们对于x左侧的部分来说,添加这种多余相邻层之间的移动,最多只能连续添加两组。
由此我们可以完成左侧的dp操作。

cost_clear[i]为直接杀死第i层boss的装载子弹时间消耗
cost_rest[i]为第一次不杀死boss第二次杀死的装载子弹时间消耗
cost_min[i]为第i层杀死boss的最优装载子弹时间消耗
dp_pre[i]为从第一层到达第i层且杀死所有boss的最少时间消耗

dp_pre[i]的转移方程即为:
首先在基础方案为直接从第i-1层走过来:
dp_pre[i]=dp_pre[i-1]+cost_clear[i]
意思为前面i-1层采用最优,第i层直接杀死boss。
然后考虑i-1层和第i层之间添加一组移动:
dp_pre[i]=min(dp_pre[i],dp_pre[i-2]+2 × \times ×d+cost_min[i-1]+cost_min[i])
再考虑在i-2和i-1层,i-1层和第i层之间各添加一组移动:
dp_pre[i]=min(dp_pre[i],dp_pre[i-3]+4 × \times ×d+cost_min[i-2]+cost_min[i-1]+cost_min[i])

dp_pre[n]的情况,实际上就已经把上面讨论x层右侧时候,第n层的优化问题包括进去了,因为对第n层优化,就可以看做x=n的情况。

计算出dp数组之后,我们从第n层从后往前扫,前i-1层使用dp_pre的值,i层到n层使用最上面分析的来回一次的方案,该值可以在循环中O(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=1e6+7;

ll n,r1,r2,r3,d;
ll num[maxn];
ll cost_clear[maxn],cost_rest[maxn],cost_min[maxn];
//clear代表一次直接杀死boss所需的时间消耗,rest为分两次杀死boss的消耗,min为clear和rest中的较小值
ll dp_pre[maxn];

int32_t main()
{
    IOS;
    cin>>n>>r1>>r2>>r3>>d;
    for(ll i=1;i<=n;i++)
    {
        cin>>num[i];
        cost_clear[i]=num[i]*r1+r3;
        cost_rest[i]=min(r2+r1,r1*num[i]+r1*2);
        cost_min[i]=min(cost_clear[i],cost_rest[i]);
    }
    ll ans=d*(n-1);//不论哪种方案我们都必然要先从第1层走到第n层,有n-1次转移,先加到答案上
    for(ll i=1;i<=n;i++)
    {
        dp_pre[i]=cost_clear[i]+dp_pre[i-1];
        if(i>1) dp_pre[i]=min(dp_pre[i],dp_pre[i-2]+2*d+cost_min[i-1]+cost_min[i]);
        if(i>2) dp_pre[i]=min(dp_pre[i],dp_pre[i-3]+4*d+cost_min[i-2]+cost_min[i-1]+cost_min[i]);
    }
    ll add=dp_pre[n],sum_suf=cost_clear[n];//suf为第x层右侧部分的消耗
    for(ll i=n-1;i;i--)
    {
        sum_suf+=d+cost_min[i];
        add=min(add,sum_suf+dp_pre[i-1]);
    }
    cout<<ans+add<<endl;
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值