Codeforces Round #667 (Div. 3)A-F题解

Codeforces Round #667 (Div. 3)A-F题解
//写于rating值2033/2184
//第二天起来补的题

比赛链接:https://codeforces.com/contest/1409

A题
水题
题意为给你两个整数a和b,你每次可以对a进行增加1-10或者减少1-10的操作,问你最少需要多少次数可以让a的值变为b。

这里直接采取简单的贪心想法。假设a初始的情况是小于b,假设我们的操作过程中某一次操作使得a的值大于b。比如a=b-3的时候,我们对他进行了+10的操作,那么a=b+7。我们最少还需要进行一次-7的操作才能让a变成7。这一过程中进行了+10和-7两次操作,然而这两次操作完全可以合并为一个+3的操作。
实际上,我们所有的操作中,如果出现了+和-的操作,那么这两个操作必定是可以合并为一次操作。

直接计算两者之间的差值是多少,贪心使用+10或者-10的操作,最后个位如果还有剩再多增加一次操作即可。

以上分析过程对于一个合格的选手来说在几秒内就可以完成。

#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 a,b;
        cin>>a>>b;
        a=abs(a-b);
        cout<<a/10+(a%10!=0)<<endl;
    }
}

B题
贪心,简单结论

题意为给定5个整数a,b,x,y,n。初始的时候a不小于x,b不小于y。现在对a或者b总共最多进行n次的减一操作,且需要保证a最后仍然不小于x,b不小于y。询问最后a × \times ×b的最小值是多少。

这里首先要用贪心推一个小结论。
我们先不考虑最后的a × \times ×b最小这个条件,只考虑a>=x和b>=y这个条件,思考我们最多可以减少多少次,设我们最多可以减m次。
对于所有的减少次数少于m次的情况,容易用贪心的想法证明在减少m次的情况中必然有对应的a × \times ×b值更小的情况。理由是a和b仍然存在可以继续减少的空间。

由此我们推出结论,减少的次数必然就是最多的减少次数m次。再考虑此时的最优情况。

再就是应用一个简单的数学结论,当a+b的值确定时,当a和b的差值越接近时,a × \times ×b的值对应更大。
我们希望a × \times ×b的值最小,那么我们尽可能要让a和b的差值更大。既然要让a和b的差值尽可能大,总的减少次数又确定了,那我们在保证a>=x和b>=y的情况下,分优先考虑减少a和优先考虑减少b两种情况,分别计算取较小值即可。

#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 solve(ll a,ll b,ll x,ll y,ll n)
{
    
    
    ll temp=min(a-x,n);//优先考虑减a的值,temp为a能减少的最大值
    a-=temp;n-=temp;
    temp=min(b-y,n);//再考虑剩下的次数全部减到b上
    b-=temp;
    return a*b;
}

int32_t main()
{
    
    
    IOS;
    int t;
    cin>>t;
    while(t--)
    {
    
    
        ll a,b,x,y,n;
        cin>>a>>b>>x>>y>>n;
        ll ans1=solve(a,b,x,y,n);//ans1为优先减a的值的结果
        ll ans2=solve(b,a,y,x,n);//ans2为优先减b的值的结果
        cout<<min(ans1,ans2)<<endl;
    }
}

C题
特殊数据范围观察,贪心,构造

题意为你需要构造一个长度为n的正整数等差数列。初始的时候你已经知道这个数列必定存在两个正整数x和y(x<y),现在需要你构造出一种数列(有多种答案,任意种均可)(实际上排序后只有一种),使得数列中的最大值尽可能小。

注意到这道题的n数据范围非常小,我们可以从n做一点文章。

对于我们最后构造的数列来说,x和y两个数字已经确定在里面了,最后的最大值最小也要等于y。
我们先用贪心的思路去考虑,对与[x+1,y-1]和[1,x-1]这两个范围,我们构造数字是不会影响数列的最大值为y的。当这两个范围无法构造后,我们再去考虑构造大于y的部分。
确定这样的基本思路后,再考虑我们构造的是一个等差数列,这个差值dis,如果取得越小,那我们在[x+1,y-1]和[1,x-1]这两个范围中能构造的数字数量就越多,并且当这两个范围无法构造后,我们构造大于y的部分时,也能使得最后的最大值尽可能小。
由此我们只需要计算出等差数列的差值dis最小值最小值是多少即可。注意到等差数列中必须包含x和y,因此dis必然满足是y-x的因子。而整个数列是n个数字,数字之间的间隔最多只有n-1个。因此dis应该是满足(y-x)/dis<=n-1的条件下的y-x的最小因子,也就是(y-x)/dis要尽可能大。
最开始的时候已经注意到n的数据范围非常小,因此我们直接暴力从n-1枚举(y-x)/dis的值即可。

计算出dis的值后再按照上述推导的构造思路构造即可。

#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 n,x,y;
        cin>>n>>x>>y;
        vector<ll>ans;//ans为我们最后输出的构造数列
        ans.push_back(x);ans.push_back(y);
        ll dis=0;//dis为y-x的值在满足(y-x)/dis<=n-1条件下的最小因子,也是排序后的数列中相邻两个数字的差值
        //因为总共只有n个数字,其中的间隔最多只有n-1个。我们尽可能让dis更小,使得最后数列中的最大值尽可能小
        for(ll i=n-1;i;i--)//计算y-x的值在满足(y-x)/dis<=n-1条件下的最小因子
            if((y-x)%i==0) {
    
    dis=(y-x)/i;break;}
        for(ll i=x+dis;i<y;i+=dis)//先把值大小在x和y之间的数字放进最终数列中
            ans.push_back(i);
        for(ll i=x-dis;i>0;i-=dis)//当数列长度不够的时候,先考虑比x小的数字
            //注意从x开始往下减,因为我们要保证最后的数列是个等差数列
            //先考虑比x的部分,不会影响最大值,此时的最大值仍然是y
        {
    
    
            if(ans.size()==n) break;//长度足够则跳出循环
            ans.push_back(i);
        }
        for(ll i=y+dis;;i+=dis)//长度仍然不够,只能增大数列的最大值,从y+dis开始
            //依次增加,并推入结果数列中
        {
    
    
            if(ans.size()==n) break;
            ans.push_back(i);
        }
        for(ll i=0;i<ans.size();i++)
        {
    
    
            if(i) cout<<' ';
            cout<<ans[i];
        }
        cout<<endl;
    }
}

D题
构造,贪心,简单数学规律

题意为给定两个正整数n和s,你需要进行若干次操作后,使得n的十进制上各个数字的值累加结果不超过s。每次操作你可以使得n的值+1,现需要你求出最小操作次数。

求最小的操作次数,其实也就是求一个最小的x值满足x>=n,且x的十进制上各个数字累加结果不超过s。

首先我们要推出一个简单的结论,我们对n进行+1操作后,如果没产生进位,那么n的各个十进制上数字的累加和sum必然是增加的;如果产生了进位,那么sum必然是减少的。
这个结论推导出来非常简单,可以自行在稿纸上验证一下,不再赘述证明过程。

另外注意到,我们累加和进位的过程中,先改变的都是位数较低的部分,位数高的部分是后改变的。
我们从十进制最高位开始,累加各位上的数字,找到小于s的最大下标tar,并且记录累加值sum。
如果sum<=s那么我们直接输出0不需要进行+1操作,初始值已经满足条件。
如果sum>s那么代表我们需要进行若干次累加操作使得sum值<=s,对于我们记录的tar下标后面的部分,需要全部被清0才可能满足sum值<=s,此时会对tar下标位置进位1,而进位1后,tar下标前的累加值最多只会增加1,仍然满足<=s。

由此我们只需要寻找n从十进制最高位开始,累加各位上的数字,找到累加值小于s的最大下标tar。进行上述操作即可:

#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> change(ll x)//把整数x按照十进制转换成vector数组返回
{
    
    
    vector<int> s;
    stack<int>S;
    while(x) {
    
    S.push(x%10);x/=10;}
    while(S.size()) {
    
    s.push_back(S.top());S.pop();}
    return s;
}

int32_t main()
{
    
    
    IOS;
    int t;
    cin>>t;
    while(t--)
    {
    
    
        ll n,s;
        cin>>n>>s;
        vector<int> sn=change(n);
        int tar=-1,sum=0;//tar为满足下标tar前面所有十进制位上的值累加满足小于s的最大下标位置
        //sum为当前的十进制位数值累加和
        for(int i=0;i<sn.size();i++)//寻找tar的位置
        {
    
    
            sum+=sn[i];
            if(sum>s) break;
            if(sum<s) tar=i;
        }
        if(sum<=s) cout<<0<<endl;//如果整个数列的十进制位上数字累加不超过s,那么不需要进行任何操作
        else
        {
    
    
            ll ans=0;//ans为我们最后构造的满足条件的最小结果
            if(tar==-1)//如果tar=-1,代表第一位上的数字就已经超出s限制了,直接构造10000...00,0的个数为原数字十进制长度
            {
    
    
                ans=1;
                for(int i=0;i<sn.size();i++) ans*=10;
            }
            else
            {
    
    
                for(int i=0;i<=tar;i++)//下标tar前的数字保持原本数值
                    ans=ans*10+sn[i];
                ans++;//tar位置后面的部分通过不断增加的操作,累加后进位1到下标tar位置
                for(int i=tar+1;i<sn.size();i++)//后面的十进制位上全部置零
                    ans*=10;
            }
            cout<<ans-n<<endl;
        }
    }
}

E题
双指针算法,dp

题意为在一个二维坐标平面上有n个点(所有数据组的n累加不超过2e5,且横纵坐标均为整数),你现在可以在这个坐标平面上放两块长度为k的挡板,然后这个平面上的点会不断竖直向下移动,撞到挡板上就停下来(撞到挡板的边缘也算)。问你最多可以用这两块挡板挡住多少个点。

这里首先精简下题意,思考一下会发现纵坐标y是没有任何用处的。我们只需要关注点的横坐标即可。

然后需要得到一个简单的结论(自己大脑里或者稿纸上想想,此处不做证明),最优的情况下,挡板的位置是可以适当左右移动,直到某个被包括的点移动到了挡板的左端点或者右端点则无法再右移或者左移。
得到这个结论后,我们可以统一规定挡板都移动到了右端点包含了一个点的情况。然后就转化为了一个基础的线性dp模型,我们用dp[i]记录以横坐标i为一块挡板的右端点,该块挡板能挡住的点数。
转移方程为dp[i]=max(dp[i-1],sum[i]-sum[i-k-1]);其中sum[i]数组为前缀和数组记录横坐标小于等于i的点的数量。

但是这道题的数据范围告诉我们横坐标的数据范围为1e9,明显不能用上述方法来完成,时空复杂度都不行。

我们在上面推出了,挡板的右端点必然是某一个点的横坐标,那么我们可以枚举右断点横坐标是哪一个点的横坐标,关键问题就在于如何解决左端点的横坐标,对应在排序后的点里位于怎样的位置下标。
这里我们可以采取双指针算法,或者用二分应该也可(效率会低,不知道会不会被tle,没试过)。

我们直接暴力for挡板右端点的横坐标是哪一个点的横坐标,用tarr记录,而tarl记录的是左侧端点的位置。然后在循环++右端点下标的过程中,添加一个限制tarl和tarr对应的点的横坐标差值不能超过k,如果超过了则对tarl进行右移操作(点已经按照横坐标升序排序)。

具体操作看代码吧,如果难以理解可以先看注释掉的部分。

#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 node[maxn];//记录点的横坐标数据,按照x的增序进行排序
ll n,k,temp;
ll len[maxn];//len[i]记录node数组中第i个点作为长度为k的挡板的右端点,该挡板可以挡住多少点,用于后面的dp过程

int32_t main()
{
    
    
    IOS;
    int t;
    cin>>t;
    while(t--)
    {
    
    
        cin>>n>>k;
        for(int i=0;i<n;i++) cin>>node[i];
        for(int i=0;i<n;i++) cin>>temp;
        sort(node,node+n);
        ll tarl,tarr;//tarr指示我们当前挡板的右端点是node数组中的哪一个点,tarl指示当前挡板左端点是node数组中的哪一个点
        ll ans=0,Max=0;//ans为我们最后的答案,两块挡板能挡住的最多点数。Max记录对于tarl左侧的点,一块长度为k的挡板能挡住的最多点数,用于dp过程。
        for(tarr=tarl=0;tarr<n;tarr++)
        {
    
    
            while(node[tarl]<node[tarr]-k)//左端点与右端点的横坐标需要满足条件差值不超过k
            {
    
    
                Max=max(Max,len[tarl]);//dp过程,tarl需要右移,此时要更新Max记录的值
                tarl++;
            }
            len[tarr]=tarr-tarl+1;//计算len[tarr],为node数组中第tarr个点作为长度为k的挡板的右端点,该挡板可以挡住的点的数量
            ans=max(Max+len[tarr],ans);//更新ans值,Max为tarl左侧的那块挡板挡住的点数,len[tarr]为当前挡板挡住的点数,两者相加即为当前情况的最优值
        }
//        for(tarr=tarl=0;tarr<n;tarr++)
//        {
    
    
//            while(node[tarl]<node[tarr]-k) tarl++;
//            len[tarr]=tarr-tarl+1;
//        }
//        for(tarr=tarl=0;tarr<n;tarr++)
//        {
    
    
//            while(node[tarl]<node[tarr]-k) {Max=max(Max,len[tarl]);tarl++;}
//            ans=max(Max+len[tarr],ans);
//        }
        cout<<ans<<endl;
        for(int i=0;i<n;i++) len[i]=0;
    }
}

F题
特殊数据范围,三维dp

题意为给出两个字符串s和t,其中字符串s的长度为n,字符串t的长度为2。
现在你最多对字符串s中进行k次替换字符,每次替换可以任意选择字符串s中的一个位置,把那个位置上的字符替换成任意一个字符。
现在需要使得字符串s中等于字符串t的子串(下标不一定连续)数量尽可能多,求出最多的等于字符串t的子串数。

注意到这道题的n数据范围非常小,最大只有200,必然在n上有文章可做。要么是高维的dp数组,要么是暴力的贪心法。
一开始想的是暴力贪心的做法,但是想了十多分钟中间的细节结论仍然不够清晰,放弃了贪心的写法。(不确定这道题能否用贪心写)
然后转向dp的思路,对于我们现在的这个问题,如果要用dp来写的话,就必然要把这个问题的所有情况用几个变量固定为一个个确定的情况,并且找到这些情况之间的转移关系。

首先比较容易想到的,平时线性dp的转移过程,我们可以用dp[i][j]记录字符串s中的前i个字符,改变其中的j个字符后能得到的最多数量的子串t的个数。
但是我们注意到仅仅用dp[i][j]限制了前i个字符,改变j个字符是无法确定为某一类特定情况的,因为改变的这j个字符具体该怎么变情况是不固定的,并且最关键的一点,这样的dp数组我们无法推出状态之间的转移关系。

然而我们前面已经注意到n的数据范围非常小了,我们完全可以对dp数组再多加一个维度。并且我们需要注意到,当我们计算完字符串s的前i个字符能构成的子串t的个数后,其中不等于t[0]的字符都已经失去了作用,只有其中等于t[0]的字符可能与之后的t[1]字符组成子串t。我们只需要关注前i个字符进行j次改变后,其中有几个t[0]即可。
因为我们将dp数组扩展到三维dp[i][j][l],i代表字符串s的前i个字符,j代表对这i个字符改变了j次,l代表改变j次后这i个字符中等于字符t[0]的个数,即可表示出一种确定的状态,且这些状态是可以转移的。

从i到i+1的转移方式主要分成六种如下:(具体转移方程见代码)
第一大类不改变s[i+1]字符,三种情况:
s[i+1]==t[0]
s[i+1]==t[1]
s[i+1]!=t[0]&&s[i+1]!=t[1]
第二大类改变s[i+1]字符,三种情况:
改变s[i+1]为t[0]
改变s[i+1]为t[1]
改变s[i+1]为既不是t[0]也不是t[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 dp[203][203][203];//dp[i][j][l]代表字符串s中前i个字符改变其中的j个字符,并且得到了l个t[0]字符的情况下,这前i个字符构造的最多子串t的数量
ll n,k;
char s[203],t[3];

int32_t main()
{
    
    
    IOS;
    memset(dp,-1,sizeof(dp));//dp数组初始化为-1,-1代表该种情况不可能发生
    cin>>n>>k>>(s+1)>>t;
    if(t[0]==t[1])//特殊处理t中两个字符相同的情况
    {
    
    //此处不做证明,很容易推得
        ll sum=0;
        for(ll i=1;i<=n;i++)
            if(s[i]==t[0]) sum++;
        sum=min(sum+k,n);
        cout<<(sum-1)*sum/2<<endl;
    }
    else
    {
    
    
        dp[0][0][0]=0;//初始化情况,一个字符也不选,一个字符也没被改变,一个t[0]字符也不存在时对应的数量是0
        for(ll i=0;i<n;i++)//从dp[i]向dp[i+1]转移状态
        {
    
    
            for(ll j=0;j<=k;j++)//暴力枚举j和l的值,注意改变次数j不能超过上限k
            {
    
    
                for(ll l=0;l<=i;l++)//l代表前i个字符中t[0]的个数,当然也不能超过字符总数i
                {
    
    
                    if(dp[i][j][l]!=-1)//如果当前情况是可能发生的,则向dp[i+1]状态转移
                    {
    
    
                        //转移不改变第i+1个字符的状态,根据s[i+1]是否等于t[0]或者t[1]分为三种情况转移
                        if(s[i+1]==t[0]) dp[i+1][j][l+1]=max(dp[i+1][j][l+1],dp[i][j][l]);
                        else if(s[i+1]==t[1]) dp[i+1][j][l]=max(dp[i+1][j][l],dp[i][j][l]+l);
                        else dp[i+1][j][l]=max(dp[i+1][j][l],dp[i][j][l]);
                        //转移改变第i+1个字符的状态
                        if(j<k)//这里当然有个限制,总的改变次数不能超过k,因此前i个字符的改变次数必须小于k
                        {
    
    
                            dp[i+1][j+1][l+1]=max(dp[i+1][j][l+1],dp[i][j][l]);//改变s[i+1]为t[0]
                            dp[i+1][j+1][l]=max(dp[i+1][j][l],dp[i][j][l]+l);//改变s[i+1]为t[1]
                            //这里本应还有一个改变s[i+1]为既不等于t[0]又不等于t[1]的字符,但是明显的这种操作是
                            //废操作,可以直接忽略不写
                        }
                    }
                }
            }
        }
        ll ans=0;
        for(ll i=1;i<=200;i++)
            for(ll j=0;j<=200;j++)
                for(ll l=0;l<=200;l++)
                    ans=max(ans,dp[i][j][l]);
        cout<<ans<<endl;
    }
}

猜你喜欢

转载自blog.csdn.net/StandNotAlone/article/details/108414743
今日推荐