Codeforces Round #708 (Div. 2) 赛后补题

比赛传送门

B题传送门

题解:

  本题其实只要将所有的数读进来,将每个数模上m后的数值的个数存下来就好了。利用\(map\)来记录,这样我们只要通过\(map\)记录下来的个数来计算我们的结果。
  假设数模上\(m\)后的值为\(x\)。
  ①当\(x\)等于\(0\)的时候,我们就可以把所有\(x=0\)的数值放到同一个组中。
  ②当\(x\)不等于\(0\)的时候,我们可以放到一起的情况就是\(x,m-x,x,....\),这样的一组中,\(x\)与\(m-x\)的个数差为1,那么如果其中一个的个数偏多的话,多出的部分都是单独一组,也就是\(abs(cnt[x]-cnt[m-x])-1\)。

代码:
code
#include<bits/stdc++.h>

using namespace std;

int main()
{
    int t;
    cin>>t;
    while(t--)
    {
        int n,m;
        scanf("%d%d",&n,&m);
        map<int,int> cnt;//cnt用来记录
        for(int i=0;i<n;i++)
        {
            int x;
            scanf("%d",&x);
            cnt[x%m]++;
        }
        int ans=0;
        for(auto it=cnt.begin();it!=cnt.end();it++)
        {
            if(it->first==0) ans++;
            else if(it->first*2==m) ans++;
            else
            {
                if(it->first*2<m||cnt.find(m-it->first)==cnt.end())
                {
                    int x=it->second,y=cnt[m-it->first];
                    ans+=1+max(0,abs(x-y)-1);
                }
            }
        }
        cout<<ans<<endl;
    }
    return 0;
}

D题传送门

题解:

  本题其实比较容易看出是动态规划的问题,但是题目中的内存限制为\(32mb\),这样我们就不能开一个二维的\(dp\)数组来计算所有的可能性。
  最开始的时候我没有看\(n\)的限制,以为是一个状态压缩\(dp\)(状压\(dp\)数据范围小),等想到一半看了一眼n的范围就知道肯定不是状压\(dp\)了,最后还是去看了官方的\(Tutorial\).下面就是官方给出来的思路:
  根据数据我们可以构造出一个图,其中节点就是我们的要解决的问题,那么对于节点\((i,j)\)来说,边就是\(|c_i-c_j|\),并且这个值还是独一无二的。为什么这个值是独一无二的呢,这里我们每个问题的复杂度(题目给出)的形式是很特殊的,如果我们用\(i_{th}\)节点的c减去\(j_{th}\)节点的\(c\),我们就会得到一个二进制从\(j\)到\(i-1\)的位置上全是\(1\)的一个数,也就是说对于二进制中\(k_{th}\)位\((j <= k < i)\)来说,它都是\(1\),于是对于不同的\((i,j)\)来说,他们的权值就是独一无二的。
  我们来定义一下dp数组代表什么意思,\(dp[i]\)表示在解决问题i后能够获得的分数。最开始初始化的时候,\(dp\)数组应该全部为\(0\)(由题目计算分数的方式得出)。由于在解决问题的过程中\(IQ\)会不断增长,我们要获得多的分数也需要解决更多的问题,那么就需要从小到大计算边。具体我们只要对于每个\(j\)\((2<j<n)\) 时,\(i\)从\(j-1\)开始递减即可(思考一下为什么)。
  接下来我们需要思考一下如何更新\(dp\)数组中的值,首先是两个问题的\(tag\)是不能一样的,所以我们就不需要计算这种情况。计算当前\(i\)和\(j\)的时候,我们是先解决\(i_{th}\)问题还是\(j_{th}\)问题,权值是不会变化的,也就是\(dp[i]=max(dp[i],dp[j]+p)\)与\(dp[j]=max(dp[j],dp[i]+p)\)要同时进行更新。由于这里会套用两个\(dp\)的值,而且会更新,所以我们可以用两个变量先存一下就好。

代码:
code
#include<bits/stdc++.h>
using namespace std;
int main()
{
    int t;
    cin>>t;
    while(t--)
    {
        int n;
        scanf("%d",&n);
        vector<long long> tags(n),s(n),dp(n,0);
        for(int i=0;i<n;i++) scanf("%lld",&tags[i]);
        for(int i=0;i<n;i++) scanf("%lld",&s[i]);
        for(int j=1;j<n;j++)
        {
            for(int i=j-1;i>=0;i--)
            {
                if(tags[i]==tags[j]) continue;
                long long p=abs(s[j]-s[i]);
                long long dpj=dp[j],dpi=dp[i];
                dp[i]=max(dp[i],dpj+p);
                dp[j]=max(dp[j],dpi+p);
            }
        }
        printf("%lld\n",*max_element(dp.begin(),dp.end()));
    }
    return 0;
}

E1. Square-free division (easy version)

题解:

  本题是需要我们去判断任意两个数是否相乘后会变成一个完全平方数,如果我们使用暴力的办法将所有加入队列的数和目前这个数去一一判断的话,时间复杂度极高,最坏情况下会达\(O(N^3)\),就无法在题目要求的范围内完成。
  本题需要知道的知识点,完全平方数、质因数以及质因数分解
  由质因数我们可以知道,任意一个合数都是有一个独一无二的质因子分解式。因此每个数其实我们都可以用他们的质因子分解式来替代。那么什么时候两个数相乘的结果会是一个完全平方数呢?我们先来看一下题目中的例子,比如说\(6=2^1*3^1\),而\(24=3^1*2^3\),当\(6*24=144\)的时候,\(144=3^2*2^4\),,这个时候\(144\)是一个完全平方数,因为\(3\)和\(2\)的指数都是偶数,我们继续取\(8\),得到\(8=2^3\),当\(8*24=192\)时,\(192=3^1*2^4\),但是\(192\)并不是一个完全平方数,其质因数分解式的指数并不全为偶数。
  从上面的分析我们可以知道,如果两个数的乘积是完全平方数的话,这两个数就需要满足其质因数分解式相乘后各项因子的指数为偶数,既然如此,我们就定义一个\(mask\)函数,让\(mask(x)=p_1^{k_1mod2}p_2^{k_2mod2}...p_n^{k_nmod2}\),因此,只要两个数的\(mask\)是相等的,那么我们就知道这两个数相乘的乘积为完全平方数。
  所以在最开始,我们需要预处理出数据范围内的素数,然后将记下每个数的质因数分解式中最大的素数,用一个数组来存储,这样我们之后计算\(mask\)的时候就可以直接得到想要的素数。完成预处理后,我们将每个数读进来,将每个数的\(mask\)计算出来,用一个\(cnt\)来记录某个素数的指数,如果是奇数的话,我们就将\(mask\)乘上该素数。最后计算答案的时候,我们只要用贪心的思想,如果不能加入当前分段的话,我们就新开一个分段,并结束当前分段的计算。

代码:
code
#include<bits/stdc++.h>

using namespace std;

const int N=1e7;

vector<int> primes;
int mind[N+1];

int main()
{
    //预处理
    for(int i=2;i<=N;i++)
    {
        if(mind[i]==0)
        {
            primes.push_back(i);
            mind[i]=i;
        }
        for(auto &x:primes)
        {
            if(x>mind[i]||x*i>N) break;
            mind[i*x]=x;
        }
    }

    int t;
    scanf("%d",&t);
    while(t--)
    {
        int n,k;
        scanf("%d%d",&n,&k);
        vector<int> a(n,1);//我们的mask数组
        for(int i=0;i<n;i++)
        {
            int x;
            scanf("%d",&x);
            //现在要对x进行拆分
            int cnt=0;//用来记录每个素数的个数
            int last=0;//记录最后一个标记到的素数
            while(x>1)
            {
                int p=mind[x];
                if(p==last) cnt++;//这个素数的个数增加一个
                else
                {
                    if(cnt%2==1)//我们要求的是让指数在0到1这个区间,如果是奇数的话,说明可以留下
                        a[i]*=last;
                    cnt=1;
                    last=p;
                }
                x/=p;
            }
            if(cnt%2==1)
            {
                a[i]*=last;
            }
        }

        int L=0,ans=1;//从第一个开始
        map<int,int> last;
        for(int i=0;i<n;i++)
        {
            if(last.find(a[i])!=last.end()&&last[a[i]]>=L)
            {
                L=i;
                ans++;
            }
            last[a[i]]=i;
        }
        printf("%d\n",ans);
    }
    return 0;
}

E2. Square-free division (hard version)

题解:

  本题是\(E1\)的困难版本,与\(E1\)不同的是,本题允许我们做\(k\)次的修改,可以将数组中任意一个数变成另外一个正数。让我们求出经过不超过\(k\)次修改后的数组能够分成的最小分段数。
  如何判断两个数的乘积是否为完全平方数的方法,我们在\(E1\)中已经了解过了,所以我们这里继续使用\(mask(x)=p_1^{k_1mod2}p_2^{k_2mod2}...p_n^{k_nmod2}\),接下来我们就需要思考如何进行不超过\(k\)次的操作对数组的影响。
  容易想到的是,我们可以利用深度优先搜索,将所有可能的修改位置的情况都枚举一遍,记录下每种情况下我们可以获得的最小分段数。但是对于题目中给出的数据范围来说,这样的办法是一定会超时的,而且在实现上,我们是需要重新修改我们对应\(pos\)上数值的\(mask\)值的。
  于是,我们想到可以利用动态规划的思路来枚举情况,定义\(dp_{i,j}\)为经过不超过\(j\)次修改后,从\(1\to i\)能够得到的分段数,\(dp\)数组的属性是得到最小值。接下来我们需要思考用什么来进行状态划分,本题有点类似石子合并的思路,对于\(dp_{i,j}\),我们需要枚举\(\left\{{0,j}\right\}\)间所有的修改情况,所以我们还缺乏一个数组用来记录不同位置数值修改后对数组分段的影响。
  我们现在定义一个\(left_{i,j}\)数组,我们找到一个\(p\),也是我们\(left\)数组中需要存储的值,在经过\(j\)次修改后,让\(a_p,a_{p+1},a_{p+2},...,a_{i}\)间不存在\(mask\)相同的两个数。对于一个确定的\(j\),如果我们让\(i\)增大,那么\(left_{i,j}\)也会增大,因此对于一个确定的\(j\),我们可以利用双指针的办法计算出\(left_{i,j}\),这里算法时间复杂度为\(O(nk)\)。
  现在有了left数组后,我们继续思考\(dp_{i,j}\)的状态转移方程,首先\(dp_{i,j}\)可以从\(dp_{i,j-1}\)转移而来,然后我们还需要枚举从\(\left\{{0,j}\right\}\)之间的状态,假设\(p=left_{i,x}\),那么\(dp_{i,j}=min(dp_{i,j},dp_{p-1,j-x}+1)\)。该算法的时间复杂度为\(O(nk^2)\),所以整体算法的时间复杂度为\(O(nk^2)\),可以满足题目的时限。

代码:
code
#include<bits/stdc++.h>

using namespace std;

const int N=1e7;
const int INF=0x3f3f3f3f;

vector<int> primes;
int mind[N+1];

int main()
{
    //第一步操作和E1是一样的,计算出mind
    for(int i=2; i<=N; i++)
    {
        if(mind[i]==0)
        {
            mind[i]=i;
            primes.push_back(i);
        }
        for(auto &x:primes)
        {
            if(x>mind[i]||x*i>N) break;
            mind[x*i]=x;
        }
    }

    int t;
    cin>>t;
    vector<int> cnt(N+1);
    while(t--)
    {
        int n,k;
        scanf("%d%d",&n,&k);
        vector<int> mask(n,1);
        for(int i=0; i<n; i++)
        {
            int x;
            scanf("%d",&x);
            int cnt=0,last=0;
            while(x>1)
            {
                int p=mind[x];
                if(p==last) cnt++;
                else
                {
                    if(cnt&1) mask[i]*=last;
                    last=p;
                    cnt=1;
                }
                x/=p;
            }
            if(cnt&1) mask[i]*=last;
        }


        vector<vector<int>> left(n,vector<int>(k+1));
        for(int j=0; j<=k; j++)
        {
            int p=n,now=0;
            for(int i=n-1; i>=0; i--)
            {
                while((p-1)>=0 &&(now + (cnt[mask[p - 1]] > 0)) <= j)
                {
                    p--;
                    now+=(cnt[mask[p]]>0);
                    cnt[mask[p]]++;
                }
                left[i][j]=p;
                if(cnt[mask[i]]>1) now--;
                cnt[mask[i]]--;
            }
        }

        vector<vector<int>> dp(n+1,vector<int>(k+1,INF));
        for(auto &c:dp[0]) c=0;
        for(int i=1; i<=n; i++)
        {
            for(int j=0; j<=k; j++)
            {
                if(j>0) dp[i][j]=dp[i][j-1];
                for(int x=0; x<=j; x++)
                {
                    dp[i][j]=min(dp[i][j],dp[left[i-1][j-x]][x]+1);
                }
            }
        }
        int ans=INF;
        for(auto &c:dp.back()) ans=min(ans,c);
        printf("%d\n",ans);
    }
    return 0;
}

上一篇:AI换脸-简单换脸、人脸对齐、关键点定位与画图


下一篇:理解BERT模型