暑集 Day28 dp专题比赛题解

2023-08-22 10:55:44

前言

注:这场比赛全是 dp 题,时长 3 小时。

第一眼看到 A 题这种奇葩题面直接跳了,感觉不是自己这种成分能写的,结果我旁边两个呆呆鸟 zyx,cf 居然觉得我直接上来把 A 秒了,然后他们死磕 A 题嗑了两个多小时???离谱。

最后我只 A 了三道,拿了最后一题的暴力 30 分,本来以为可能会被卡的结果告诉我给的大样例就是答案???然后 yqt 这个非常邪恶的人直接输出测试数据卡了 100 多分,真的狗啊!!!墙裂谴责!!!!之后蔡lb 觉得懒得改数据(他没有这么说)就说当是给我们信心赛的福利,拿了的都是好事,离大谱。

这次 9 个人 AK,说明确实不难,也说明我实在是太菜了。。。。。
Alt text

A

Alt text
\(n\le 1000,a_i\le 1000\)

没看懂,看了题解也不是很清楚。看了 std 后也不懂有了一些新的理解。

原来是这样!就是排序去重以后从后往前 dp,每次取之前取的对于现在的最优解,然后最后看先手能转移到哪个最优解即可(看代码就懂了)。

bool vis[1005];
int n,m,a[1005],f[2][1005];
int main(){
    n=read(),m=read();
    for(int i=1;i<=n;i++){
        a[i]=read();
        if(vis[a[i]])n--,i--;
        vis[a[i]]=1;
    }
    sort(a+1,a+1+n);a[n+1]=10000;
    for(int i=n;i;--i){
        if(a[i+1]-a[i]>m){f[0][i]=a[i],f[1][i]=-a[i];}
        else{
            f[0][i]=1e9;
            for(int j=i+1;j<=n&&a[j]-a[i]<=m;j++)f[0][i]=min(f[0][i],f[1][j]+a[i]);
            f[1][i]=-1e9;
            for(int j=i+1;j<=n&&a[j]-a[i]<=m;j++)f[1][i]=max(f[0][j]-a[i],f[1][i]);
        }
    }
    int ans=-1e9;
    for(int i=1;i<=n&&a[i]<=m;i++)ans=max(ans,f[0][i]);
    cout<<ans;
}

B

Alt text
这就直接打个一眼 dp 就好了,令 \(f_{i,j}\) 表示第 \(i\) 个为 \(j\) 的方案数,转移:

\[f_{i,kj}+=f_{i_j}\ \ \ (k\in N^*,kj\le m) \]

复杂度应该是 \(O(nm\sqrt m)\)\(n,m\le 2000\),可过。

\(Code\)

const int mod=1e9+7;
int n,m;
ll f[2002][2002];
int main(){
    cin>>m>>n;
    f[0][1]=1;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            for(int k=j;k<=m;k+=j){
                (f[i][k]+=f[i-1][j])%=mod;
            }
        }
    }
    ll ans=0;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            (ans+=f[i][j])%=mod;
        }
    }
    cout<<ans;
}

C

Alt text
一眼数位 dp,做就完了。

\(Code\)

int n,mod;
ll f[2][2][2][2][2][2][2][7][1001];
ll dfs(int len,int mu,bool x0,bool x1,bool x2,bool x3,bool x4,bool x5,bool x6){
    if(!len)return (x0&x1&x2&x3&x4&x5&x6);
    if(~f[x0][x1][x2][x3][x4][x5][x6][mu][len])return f[x0][x1][x2][x3][x4][x5][x6][mu][len];
    ll res=0;
    for(int i=0;i<=9;i++){
        int o=(mu*10+i)%7;
        res+=dfs(len-1,o,x0|(o==0),x1|(o==1),x2|(o==2),x3|(o==3),x4|(o==4),x5|(o==5),x6|(o==6));
        res%=mod;
    }
    return f[x0][x1][x2][x3][x4][x5][x6][mu][len]=res;
}
int main(){
    memset(f,-1,sizeof(f));
    cin>>n>>mod;
    printf("%lld",dfs(n,0,0,0,0,0,0,0,0));
}

D

看见这个 \(k\le 10\),果断状压 dp,发现 \(n\) 不大,也不需要矩阵加速(如果矩阵加速状态数也太大了)。

那么我们用到和上次那道模拟赛的状压差不多的方法,我们考虑一个 \(k\) 位二进制数,那么第 \(k-1\) 位表示我当前这位有没有取,第 \(0\) 位表示我前面第 \(k-1\) 个有没有取(当然你也可以通过把第 \(0\) 位变当前位转移),然后令 \(f_{i,j}\) 表示扫到第 \(i\) 个,状态为 \(j\) 的答案,令 \(g_{i,j}\) 表示扫到第 \(i\) 个,状态为 \(j\) 的方案数,\(count_j\) 表示状态 \(j\) 中被选的数的个数,那么转移很好想到,为:

\[f_{i,(j>>1)|(1<<k-1)}=\sum_{count_{j}\in[L,R]}^{count_{(j>>1)|(1<<k-1)}\in [L,R]}(f_{i-1,j}+a[i]\times g_{i-1,j})\\g_{i,(j>>1)|(1<<k-1)}=\sum_{count_{j}\in[L,R]}^{count_{(j>>1)|(1<<k-1)}\in [L,R]}g_{i-1,j}\\f_{i,j>>1}=\sum_{count_{j}\in[L,R]}^{count_{j>>1}\in [L,R]}f_{i-1,j}\\g_{i,j>>1}=\sum_{count_{j}\in[L,R]}^{count_{j>>1}\in [L,R]}g_{i-1,j} \]

前两个式子表示第 \(i\) 位选的转移,后两个式子表示第 \(i\) 位不选的转移。

然后再预处理一下前 \(k\) 位的答案即可,最后答案就为 \(\sum\limits_{j}^{count_j\in [L,R]}f_{n,j}\)

对于 \(k>n\) 的情况,我们发现做个组合数就行了,数据范围小,乱搞即可。

\(Origin\ Code\)

const int mod=1e9+7;
int n,k,L,R;
ll a[1010];
ll f[1010][1502],g[1010][1502];
inline bool check(int w){//判断是否合法
    int len=__builtin_popcount(w);
    if(len<L||len>R)return 0;
    return 1;
}
ll C(ll x,ll y){//暴力组合数
    ll res=1;
    for(ll i=1;i<=x;i++){
        res*=i;
    }
    for(int i=1;i<=y;i++)res/=i;
    for(int i=1;i<=x-y;i++)res/=i;
    return res;
}
int main(){
    n=read(),k=read(),L=read(),R=read();
    for(int i=1;i<=n;i++){
        a[i]=read();
    }
    if(k>n){//特判
        ll ans=0,w=0;
        for(int i=1;i<=n;i++){
            w+=C(n-1,i-1);
        }
        for(int i=1;i<=n;i++){
            ans=(ans+a[i]*w%mod)%mod;
        }
        cout<<ans;
        return 0;
    }
    for(int w=1;w<=k;w++){
        for(int x=0;x<(1<<k);x++){
            if(x&((1<<k-w+1)-1))continue;//如果发现第 1 位之前都有答案那就不能统计
            (f[w][x>>1]+=f[w-1][x])%=mod;
            (f[w][(x>>1)|(1<<k-1)]+=(f[w-1][x]+a[w])%mod)%=mod;
        }
    }
    for(int w=k+1;w<=n;w++){
        for(int x=0;x<(1<<k);x++){
            if(!check(x))continue;//转移过来的状态要合法
            if(check(x>>1)){//转移之后的状态也要合法
                (f[w][x>>1]+=f[w-1][x])%=mod;
                (g[w][x>>1]+=max(1ll,g[w-1][x]))%=mod;
            }
            if(check((x>>1)|(1<<k-1))){
                (f[w][(x>>1)|(1<<k-1)]+=f[w-1][x]+a[w]*max(g[w-1][x],1ll)%mod)%=mod;
                (g[w][(x>>1)|(1<<k-1)]+=max(g[w-1][x],1ll))%=mod;
            }
        }
    }
    ll ans=0;
    for(int i=0;i<(1<<k);i++){
        if(!check(i))continue;
        ans+=f[n][i];//统计答案
        ans%=mod;
    }
    cout<<ans<<endl;
}

然而这道题调了我巨久,因为我一直都不知道为什么样例二错了,直到看不出代码(摆烂了一个多小时)有什么错误去一个一个输出并模拟样例才发现,原来第 \(i\) 位的贡献不是直接加的,要乘上对应转移式的方案数,然后又调来调去调了很久,因为一开始以为方案数是什么二的平方然后写了个快速幂,又觉得快速幂的指数要取模 \(mod-1\) 什么的,对后面的调试犯下了很多错,最后这道题花了我近两个小时调完,没时间思考第一题和最后一题了(二三题太水了加起来不到半个小时秒了)。

还是感觉我这个代码写的太冗长了,看了一下 std,觉得有一些值得修改的地方(比如为 \(k>n\) 专门写了个组合数???)。

\(Refactored\ Code\)

int n,k,L,R;
const int mod=1e9+7;
ll a[1010],f[1010][1502],g[1010][1502],ans;
inline bool check(int w){
    int len=__builtin_popcount(w);
    return (len>=L&&len<=R);   
}
ll cal(int x){
    ll res=0,cnt=0;
    for(;x;x>>=1)res=(res+(x&1)*a[++cnt])%mod;
    return res;
}
int main(){
    n=read(),k=read(),L=read(),R=read();
    for(int i=1;i<=n;i++)
        a[i]=read();
    if(k>n)
        k=n,L=1,R=n;
    for(int x=0;x<(1<<k);x++){
        if(!check(x))continue;
        f[k][x]=cal(x);
        g[k][x]=1;
    }
    for(int w=k+1;w<=n;w++){
        for(int x=0;x<(1<<k);x++){
            if(!check(x))continue;
            if(check(x>>1)){
                (f[w][x>>1]+=f[w-1][x])%=mod;
                (g[w][x>>1]+=g[w-1][x])%=mod;
            }
            if(check((x>>1)|(1<<k-1))){
                (f[w][(x>>1)|(1<<k-1)]+=f[w-1][x]+a[w]*g[w-1][x]%mod)%=mod;
                (g[w][(x>>1)|(1<<k-1)]+=g[w-1][x])%=mod;
            }
        }
    }
    for(int i=0;i<(1<<k);i++){
        ans=(ans+f[n][i])%mod;
    }
    printf("%lld\n",ans);
}

好看多了。

E

Alt text
\(n,m\le 5000,0\le a_i\le 1000\)

先不慌不忙地打个深搜拿 30 分,然后发现答案似乎比方案好求,那是不是要用某种方法先算出答案,然后再根据答案去算方案啊。来不及想了,打完暴力就跑去写第一题了。赛后发现真的是先二分计算答案,然后再算答案 \(\le ans\) 的方案数,显然答案已经最小了,所以不会有小于的,那么也就是正确的方案数了。

我们可以令 \(f_{i,k}\) 表示当前第 \(i\) 位看完休息后,一共休息了 \(k\) 次的方案数。那么:

\[f_{i,k}=\sum f_{j,k-1} \]

\(j\) 满足 \(\sum_{x=j+1}^i a_x\le ans\)

维护个前缀和就好了,注意到 \(n\) 之后不能休息,不过我们可以强制它休息,给 \(m\) 加个 \(1\) 就行。

复杂度是 \(O(n^2m)\) 的,不过因为数据太水,居然也直接碾过去了,而且还跑飞快。

\(Code1\)

const int mod=1e9+7;
int n,m,a[5002],ans,pre[5002];
ll f[5002][5002];//200MB
bool check(int x){
    int t=1,res=0;
    for(int i=1;i<=n;i++){
        if(a[i]+res<=x)res+=a[i];
        else if(a[i]<=x)res=a[i],t++;
        else return 0;
        if(t>m)return 0;
    }
    return 1;
}
int cal(){
    int l=1,r=pre[n];
    while(l<=r){
        int mid=l+r>>1;
        if(check(mid))r=mid-1;
        else l=mid+1;
    }
    return l;
}
int main(){
    n=read(),m=read()+1;
    for(int i=1;i<=n;i++)
        pre[i]=pre[i-1]+(a[i]=read());
    ans=cal();
    f[0][0]=1;
    for(int i=1;i<=n;i++)
        for(int j=0;j<i;j++)
            if(pre[i]-pre[j]<=ans)
                for(int k=1;k<=m;k++)
                    f[i][k]=(f[i][k]+f[j][k-1])%mod;
    ll sum=0;
    for(int i=1;i<=m;i++)sum=(sum+f[n][i])%mod;
    printf("%d %lld\n",ans,sum);
}

瓶颈在枚举 \(j\),我们发现 \(j\) 的指针只会增大不会减小,而且对答案的贡献是一个区间,所以我们只需要维护对于每个 \(i\) 最小满足条件的位置 \(l\) 即可。那么这个区间的答案因为都能算到贡献里,所以我们用一个 \(sum_{i,j}\) 维护方案前缀和 \(\sum_{x=1}^i f_{x,j}\) 即可,那么对区间 \([l,i]\) 的贡献即为 \(sum_{i,j}-sum_{l-1,j}\)

\(Code2\)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod=1e9+7;
int read(){
    int x=0,f=1;char c=getchar();
    for(;!isdigit(c);c=getchar())if(c=='-')f=-1;
    for(;isdigit(c);c=getchar())x=(x<<3)+(x<<1)+(c^48);
    return x*f;
}
int n,m,a[5002],ans,pre[5002];
ll f[5002][5002],sum[5002][5002];//400MB
bool check(int x){
    int t=0,res=0;//m这次不变了,因为我们是从下标[1,0]开始计算,那么还是相当于转移式变成了 i 之前休息了几次。
    for(int i=1;i<=n;i++){
        if(a[i]+res<=x)res+=a[i];
        else if(a[i]<=x)res=a[i],t++;
        else return 0;
        if(t>m)return 0;
    }
    return 1;
}
int cal(){...}
int main(){
    n=read(),m=read();
    for(int i=1;i<=n;i++)
        pre[i]=pre[i-1]+(a[i]=read());
    ans=cal();
    int l=0;
    for(int i=1;i<=n;i++){
        if(pre[i]<=ans)f[i][0]=1;//i之前休息了0次
        sum[i][0]=(sum[i-1][0]+f[i][0])%mod;//0次的贡献
        while(pre[i]-pre[l]>ans)l++;
        for(int j=1;j<=m;j++){//i 之前休息了 j 次
            f[i][j]=((sum[i-1][j-1]-(l>0?sum[l-1][j-1]:0))%mod+mod)%mod;//这里注意[i,j]是从[[l,i-1],j-1]转移的,所以因为是前缀和,l 需要减一
            sum[i][j]=(sum[i-1][j]+f[i][j])%mod;
        }
    }
    ll w=0;
    for(int i=1;i<=m;i++)
        w=(w+f[n][i])%mod;
    printf("%d %lld\n",ans,w);
}

题解看了好久,最后发现它那个码风很怪导致我以为它是从 下标0开始转移的,然后导致我一开始一直 WA,后面自己思考了才知道是从下标1开始,转移式也跟我的不太一样,而且我这个地方写的是之前休息的次数了。

posted @ 2023-09-08 10:42  NBest  阅读(24)  评论(0)    收藏  举报