• NOIP2021提高组题目解析


    我真的是太菜了,....,随便点开个NOIP提高组的题目就不会做,真的是怀疑当初自己是怎么拿到省一的.....

    P7961 [NOIP2021] 数列

    先来看一道有意思的计数题,(计数题真的是我的弱项...)
    题意大概:给定数列\(v_0,v_1,...,v_m\),从\(0,1,...,m\)中选出n个组成序列\(a_i\)(可以重复选)。要求\(2^{a_0}+2^{a_1}+...+2^{a_m}\)的二进制下1的个数不大于k,问所有\(v_{a_i}\)的乘积和。
    首先我们考虑选择的序列a,因为要考虑到进位的问题,所以我们不妨规定a是不减的。接下来考虑怎么处理这个二进制下1的个数的问题。一个比较好的想法就是将当前的进位我们都停留在当前的i。就是我们把他的进位都转化成我们当前正在处理的数上,这样方便我们量化和处理。根据这些,我们可以大致的设一个状态\(f[i][j][k][num]\)表示当前选到了第i个数,数字用到了j,且之前所有小于j的进位相当于k,当前二进制和下的1的个数为num.关于状态转移,我们可以考虑数字j用了多少个,于是就有
    \(f[i][j][k][num]=\sum(C_i^x*f[i-x][j-1][k-pocent(num)+pocnet(num-x)][temp]*v[j]^x)\),其中pocent(x)表示x的二进制下1的个数,temp/2+x=num.这样就完美的解决了问题。考虑时间复杂度,\(O(n^4m)\)大概可以的。
    注意初始化,和很多的边界问题(毕竟我也卡了很久...,可能是因为我太菜了...呜呜呜!)

    #include<bits/stdc++.h>
    #define ll long long
    using namespace std;
    const int N=35,M=105,P=998244353;
    int n,m,K,v[M],po[N];
    ll f[N][M][N][N],jc[N],jc_inv[N];
    //f[i][j][k][num]表示前i个,选到了数字j,
    //当前累计到数字j的进位为k,当前的二进制数中1的个数为num的乘积和。
    inline ll power(ll x,ll y)
    {
        ll ans=1;
        while(y)
        {
            if(y&1) ans=ans*x%P;
            y>>=1;
            x=x*x%P;
        }
        return ans%P;
    }
    inline void prework()
    {
        int n=34;
        jc[0]=1;jc_inv[0]=1;
        for(int i=1;i<=n;++i) jc[i]=jc[i-1]*i%P;
        jc_inv[n]=power(jc[n],P-2); 
        for(int i=n-1;i>=1;--i) jc_inv[i]=jc_inv[i+1]*(i+1)%P;
        for(int i=0;i<=n;++i)
        {
            int ct=0,x=i; 
            while(x)
            {
                if(x&1) ct++;
                x>>=1;
            }
            po[i]=ct;
        }
    } 
    
    inline ll C(int n,int m)
    {
        return jc[n]*jc_inv[n-m]%P*jc_inv[m]%P;
    }
    
    int main()
    {
    //    freopen("1.in","r",stdin);
        scanf("%d%d%d",&n,&m,&K);
        for(int i=0;i<=m;++i) scanf("%d",&v[i]);
        prework();
    //    for(int i=1;i<=n;++i)
    //        f[i][0][i][po[i]]=power(v[0],i);
        for(int i=1;i<=n;++i)//前i个数 
            for(int j=0;j<=m;++j)//用到了数字j 
            {
                for(int k=0;k<=i;++k)//累计到当前数字j的进位 
                    for(int l=po[k];l<=n;++l)//二进制下1的个数. 
                    {
                        for(int x=0;x<=k;++x)//枚举当前数字j的个数。 
                        {
                            if(i==x&&k==i&&l==po[i]) f[i][j][k][l]=power(v[j],x);
                            else
                            {
                                if(l-po[k]+po[k-x]<=0) continue;
                                int t1=(k-x)*2;
                                if(t1<=i-x&&j>=1) f[i][j][k][l]=(f[i][j][k][l]+C(i,x)*f[i-x][j-1][t1][l-po[k]+po[k-x]]%P*power(v[j],x)%P)%P;
                                if(t1+1<=i-x&&j>=1) f[i][j][k][l]=(f[i][j][k][l]+C(i,x)*f[i-x][j-1][t1+1][l-po[k]+po[k-x]]%P*power(v[j],x)%P)%P;   
                            }
                        }
    //                    printf("%d %d %d %d %lld\n",i,j,k,l,f[i][j][k][l]);
                    }
            }
        ll ans=0;
        for(int i=0;i<=n;++i)  
            for(int j=0;j<=K;++j) 
                ans=(ans+f[n][m][i][j])%P;
        printf("%lld",ans);                        
        return 0;    
    }
    

    P7962 [NOIP2021] 方差

    接下来看这个题,也是很有趣的一道题。
    简化题意:给定数列\(a_i\),你可以进行若干次操作,每次操作都选定一个\(i,1<i<n\),然后将\(a_{i+1}+a_{i-1}-a_i\)替换掉\(a_i\)。最后要求整个数列的方差最小,输出这个最小的方差乘\(n^2\)的结果。
    首先需要将答案变一下形式,通过简单的变换可以得知最后要求的答案为\(ans=n\times\sum_{i=1}^n a_i^2-sum^2\),简单的观察发现没什么性质,就先放到这。
    之后观察这个替换到底有什么性质,\(a_i\)替换为\(a_{i+1}+a_{i-1}-a_i\),其实和容易发现是\(a_i\)两边的数与\(a_i\)的关系,考虑我们学过什么也是这个样子的?差分嘛!我们观察他们的差分有什么变化,\(a_{i-1},a_{i},a_{i+1}\)的差分为\(a_{i-1},a_i-a_{i-1},a_{i+1}-a_i\)\(a_{i-1},a_{i+1}+a_{i-1}-a_i,a_{i+1}\)的差分为\(a_{i-1},a_{i+1}-a_i,a_i-a_{i-1}\),惊奇的发现数值竟然没有发生变化,只是位置发生了变化。说明这些操作是能将差分序列就行位置上的重排。那接下来考虑差分上怎样的位置会使得上面式子的答案最小。
    这个时候就有一些考试技巧在里面了,如果实在考场上的话,完全可以观察一下样例的性质,还可以打表,多找一下规律,这样的话,可以节省思维难度。
    接下来继续推上面的式子,由于是求方差,所以我们可以将整个数列中的每一个数都减去某个数,最后的结果是不变的。那我们就将上面的式子继续修改,尽可能的想差分靠近。我们先假设\(d_i=a_{i+1}-a_i\)
    \(ans=n\times\sum_{i=1}^n a_i^2-sum^2\)
    \(=n\times\sum_{i=1}^n (a_i-a_1)^2-(\sum_{i=1}^n a_i-a_1)^2\)
    \(=n\times\sum_{i=1}^{n-1}(\sum_{j=1}^{i}d_j)^2-(\sum_{i=1}^{n-1}\sum_{j=1}^{i}d_j)^2\)
    \(=n\times\sum_{i=1}^{n-1}\sum_{j=1}^{i}\sum_{k=1}^{i}d_j*d_k-(\sum_{i=1}^{n-1}(n-i)*d_{i})^2\)
    \(=n\times\sum_{j=1}^{n-1}\sum_{k=1}^{n-1}d_j*d_k*(n-max(j,x))-\sum_{j=1}^{n-1}\sum_{k=1}^{n-1}d_j*d_k*(n-j)*(n-k)\)
    \(=\sum_{j=1}^{n-1}\sum_{k=1}^{n-1}d_j*d_k*((j+k-max[j,k])*n-j*k)\)
    \(=\sum_{j=1}^{n-1}\sum_{k=1}^{n-1}d_j*d_k*(min[j,k]*n-j*k)\)
    \(=\sum_{i=1}^{n-1}i*d_i^2*(n-i)+2*\sum_{i=1}^{n-1}\sum_{j=i+1}^{n-1}d_i*d_j*i*(n-i)\)
    观察这个式子,发现前一半的式子值是固定的,我们想要后一半的式子尽可能的小,发现这个式子中出现次数最多的乘积是中间项。所以答案最小的话,差分序列应该是呈现一个先减后增的形式。考虑先将所有的差分先排序,之后依次考虑它查到当前序列的前段还是后端,采用dp的形式,推导后的式子太麻烦,还是回到最初的式子,\(ans=n\times\sum_{i=1}^n a_i^2-sum^2\)。发现这种形式可以用dp很方便的处理,看看我们需要什么量,发现还需要保存当前所有\(a_i\)的和,于是我们设f[i][x]表示放到了第i个差分,当前所有ai的和是x的所有ai的平方的和的最大值。至于转移就很正常了。

    #include<bits/stdc++.h>
    #define ll long long
    using namespace std;
    const int N=1e4+10,M=6e6+10;
    const ll qwq=0x3f3f3f3f3f3f3f3f;
    int n,a[N],d[N],s[N],mx;
    ll f[2][M];
    int main()
    {
    //    freopen("1.in","r",stdin);
        scanf("%d",&n);
        for(int i=1;i<=n;++i) scanf("%d",&a[i]);
        for(int i=2;i<=n;++i) a[i]-=a[1];
        a[1]=0;mx=a[n];
        for(int i=2;i<=n;++i) d[i-1]=a[i]-a[i-1];
        sort(d+1,d+n);
        for(int i=1;i<n;++i)  s[i]=s[i-1]+d[i];
        memset(f,0x3f,sizeof(f));
        int u=0;
        f[u][0]=0;
        for(int i=1;i<n;++i)
        {
            if(d[i]==0) continue;
            u=u^1;
            for(int j=0;j<=mx*i;++j) f[u][j]=qwq;
            for(int j=0;j<=mx*(i-1);++j)
            {
                if(f[u^1][j]==qwq) continue;
                f[u][d[i]*i+j]=min(f[u][d[i]*i+j],f[u^1][j]+(ll)d[i]*d[i]*i+(ll)2*j*d[i]);
                f[u][j+s[i]]=min(f[u][j+s[i]],f[u^1][j]+(ll)s[i]*s[i]);
            }
        }
        ll ans=1e18;
        for(int i=0;i<=mx*(n-1);++i) 
            if(f[u][i]!=qwq) ans=min(ans,f[u][i]*n-(ll)i*i); 
        printf("%lld\n",ans);
        return 0;
    }
    
  • 相关阅读:
    ODBC连接数据库
    发送邮件
    动态控制某些字段可以修改
    动态调用SQL和存储过程
    写数据到TXT文档
    动态创建Form
    动态调用报表
    ICE学习笔记2:摸了这么些天,终于有点着道了
    如何使用 Symbol Retriever
    Win XP注册表之性能优化
  • 原文地址:https://www.cnblogs.com/gcfer/p/15757828.html
Copyright © 2020-2023  润新知