cf DP专练

决定有时间就在codeforces上从2300分以上来一道DP练习
9.8
codeforces 713 C
与poj 3666很像的一道题,这题是严格递增,先考虑非严格递增,容易想到修改后的数字就是原来的子集,所以我们可以定义dp[i][j]表示到第i个数字时,该数字在原来离散化排序后的位置是第j大的 转移就是dp[i][j]=min(dp[i][j],dp[i-1][k]+abs(a[i]-b[k]),1<=k<=j,维护下前缀就是n^2的。下面来到这题,所谓严格递增,我们只需要把a[i]-i即可做到,

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
ll dp[3010][3010];
int a[3010],b[3010];
int main(){
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;++i){
        scanf("%d",&a[i]);
        a[i]-=i;
        b[i]=a[i];
    }
    sort(b+1,b+1+n);
    int m=unique(b+1,b+n+1)-(b+1);
    memset(dp,0x3f,sizeof(dp));
    for(int i=1;i<=m;++i)
        dp[1][i]=abs(a[1]-b[i]);
    for(int i=2;i<=n;++i){
        ll tmp=dp[i-1][0];
        for(int j=1;j<=m;++j){
            tmp=min(tmp,dp[i-1][j]);
            dp[i][j]=tmp+abs(a[i]-b[j]);
        }
    }
    ll ans=0x3f3f3f3f3f3f3f3f;
    for(int i=1;i<=n;++i)
        ans=min(ans,dp[n][i]);
    cout<<ans<<endl;
    return 0;
}

9.9
cf1198D
题意:
给你一个黑白染色的矩形,你可以选择一个h*w的矩形,把里面都染成白色,cost=max(h,w),问最小花费染完所有

思路:
这题一开始感觉一定染正方形是最优的,但是如果据此转移的话,二维上的dp都无法线性转移,然后就有点挂机了
但是我们可以发现一个特性,假如,全为黑,肯定一次性染完最好,这个证明还是很简单的,如果全为白,就不染,所以我们定义dp[x1][y1][x2][y2]为做完该矩形的最小花费,向下不断递归子问题,对于黑白相间的格子,我们只需要枚举行列分割递归下去,稍微想一下就知道这三个转移足以覆盖所有情况,没有奇怪的转移,所以处理完二位前缀和后记搜一波就好了,转移O(n), 行列O(n^4)枚举,总共 O( n ^5)随便过

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int sum[55][55],dp[55][55][55][55],n;
char s[52][52];
int dfs(int x1,int y1,int x2,int y2){
    if(~dp[x1][y1][x2][y2])return dp[x1][y1][x2][y2];
    dp[x1][y1][x2][y2]=max(x2-x1+1,y2-y1+1);
    int k=sum[x2][y2]-sum[x1-1][y1]-sum[x1][y1-1]+sum[x1-1][y1-1];
    if(k==0)return dp[x1][y1][x2][y2]=0;
    else if(k==(x2-x1+1)*(y2-y1+1))return dp[x1][y1][x2][y2];
    for(int i=x1;i<x2;++i)//x
        dp[x1][y1][x2][y2]=min(dp[x1][y1][x2][y2],dfs(x1,y1,i,y2)+dfs(i+1,y1,x2,y2));
    for(int i=y1;i<y2;++i)
        dp[x1][y1][x2][y2]=min(dp[x1][y1][x2][y2],dfs(x1,y1,x2,i)+dfs(x1,i+1,x2,y2));
    return dp[x1][y1][x2][y2];
}
int main(){
    memset(dp,-1,sizeof(dp));
    scanf("%d",&n);
    for(int i=1;i<=n;++i)
        scanf("%s",s[i]+1);
    for(int i=1;i<=n;++i)
        for(int j=1;j<=n;++j)
            sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+(s[i][j]=='#');
    cout<<dfs(1,1,n,n);
    return 0;
}

9.10
cf785D
题意:给一堆()序列,定义对称的(())序列为好序列,问有多少子序列是好的

思路:
显然是计数DP,计数DP的关键是找到基准点划分状态,其实不用想多复杂,可能就是枚举某个类型就可以全部划分好,这题我们可以枚举每一个(,对于该括号左右就可以转为组合计数问题了,设该括号(个数为num1,右边)的个数为num2
则每个(的答案为 ∑ i = 0 m i n ( n u m 1 , n u m 2 ) C n u m 1 i C n u m 2 i + 1 \sum_{i=0}^{min(num1,num2)} C_{num1}^{i}C_{num2}^{i+1} i=0min(num1,num2)Cnum1iCnum2i+1 利用范德蒙恒等式
∑ i = 0 k C n i C m k − i = C n + m k \sum_{i=0}^{k} C_{n}^{i} C_{m}^{k-i}=C_{n+m}^{k} i=0kCniCmki=Cn+mk
这个很容易就可以证明了
所以最后答案等于
∑ i = 0 m i n ( n u m 1 , n u m 2 ) C n u m 1 i C n u m 2 n u m 2 − i − 1 = C n u m 1 + n u m 2 n u m 2 − 1 \sum_{i=0}^{min(num1,num2)} C_{num1}^{i}C_{num2}^{num2-i-1}=C_{num1+num2}^{num2-1} i=0min(num1,num2)Cnum1iCnum2num2i1=Cnum1+num2num21
预处理阶乘逆元就秒了

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
const int mod=1e9+7;
const int maxn=2e5+5;
int fac[maxn],inv[maxn],num1[maxn],num2[maxn];
char s[maxn];
int C(int n,int m){
    return 1ll*fac[n]*inv[n-m]%mod*inv[m]%mod;
}
int mypow(int a,int b){
    int ans=1;
    while(b){
        if(b&1)ans=1ll*ans*a%mod;
        a=1ll*a*a%mod;
        b>>=1;
    }
    return ans;
}
void init(){
    fac[1]=1;
    for(int i=2;i<maxn;++i)
        fac[i]=1ll*fac[i-1]*i%mod;
    inv[maxn-1]=mypow(fac[maxn-1],mod-2);
    for(int i=maxn-2;i>=0;--i)
        inv[i]=1ll*inv[i+1]*(i+1)%mod;    
}
int main(){
    init();
    scanf("%s",s+1);
    int len=strlen(s+1);
    vector<int>L;
    int L1=0,R1=0;
    for(int i=1;i<=len;++i){
        if(s[i]=='(')num1[i]=L1++;
        L.push_back(i);
    }
    for(int i=len;i>=1;--i){
        if(s[i]==')')R1++;
        else num2[i]=R1;
    }
    int ans=0;
    for(auto&v:L){//枚举(为计数DP的基准点
        ans=(1ll*ans+1ll*C(num1[v]+num2[v],num2[v]-1))%mod;
    }
    cout<<ans<<endl;
}

9.19
cf1316E
题意:你有 n 个人,你想从这 n 个人中选 p 个人去到不同的 p 个位置, 选 k 个人作为观众。如果第 i 个人被选为观众他的贡献就是 a[ i ],如果第 i 个人被选为第 j 个位置上的人,那么他的贡献就是 b[ i ][ j ]。问你选 p 个位置上的人和 k 个观众最大的贡献是多少。

1 <= n <= 1e5, 1 <= p <= 7, p + k <= n
   
思路:
p的数量,显然是状压DP,但我傻逼想加多一维表示当前是被选为观众、位置还是不选,虽然好像也没问题,但是这里其实可以利用贪心优化一下,排序后,排除p个人,我们肯定是选择前k个最大的,所以用dp[i][j]表示枚举到第i个人,p个人的状态集合,然后当前 i<=cnt+k的时候,表示当前这个人有必要被选为观众,否则如果前面k个已经选完了,可以不选当前为观众,然后再用当前人选择第k个位置更新他,O(p 2^p n),1e8差不多

#include<bits/stdc++.h>

using namespace std;
typedef long long ll;
const int maxn=1e5+5;
ll dp[maxn][1<<7];
typedef long long ll;
struct Node{
    int a,s[7];
    bool operator<(const Node&A){
        return a>A.a;
    }
}node[maxn];
int main(){
    int n,p,k;
    scanf("%d%d%d",&n,&p,&k);
    for(int i=1;i<=n;++i)
        scanf("%d",&node[i].a);
    for(int i=1;i<=n;++i)
        for(int j=0;j<p;++j)
            scanf("%d",&node[i].s[j]);
    sort(node+1,node+1+n);
    memset(dp,0xc0,sizeof(dp));
    dp[0][0]=0;
    for(int i=1;i<=n;++i){
        for(int j=0;j<(1<<p);++j){
            int cnt=0;
            for(int k=0;k<p;++k)
                cnt+=(bool)(j&(1<<k));
            if(i<=k+p&&i-cnt<=k)dp[i][j]=max(dp[i][j],dp[i-1][j]+node[i].a);//当前作为观众
            else dp[i][j]=dp[i-1][j];//当前啥都不选
            for(int k=0;k<p;++k)
                if(j&(1<<k))dp[i][j]=max(dp[i][j],dp[i-1][j-(1<<k)]+node[i].s[k]);//当前选k个中
        }
    }
    cout<<dp[n][(1<<p)-1]<<endl;
    return 0;
}

cf327E
题意:
给你n个权值,以及k个位置,n个权值可以随意排列,排列后从a1,a1+a2一直累加n个点,问不和k个位置重合的有多少方案数

思路:
数据很明显是状压了,而且k很小,dp[i]表示集合是i的时候的方案数,首先我们容易想到dp[i]一定是由i二进制上少一个1的所有状态累加过来。(然后我就结束了x

被卡在了迅速得到二进制的和上,下面是lowbit的妙用,从小到大枚举
sum[i]=sum[lowbit(i)]+sum[i^lowbit(i)],相当于真子集的转移,然后可以通过lowbit枚举每一个1,学到了学到了,然后取模用if运算还可以再卡卡常,这里位运算的妙用真的学到了

#include<bits/stdc++.h>

using namespace std;
typedef long long ll;
const int mod=1e9+7;
int dp[1<<24],a[25],id[2];
ll sum[1<<24];
inline int lowbit(int x){
    return x&(-x);
}
int main(){
    int n,k;
    scanf("%d",&n);
    for(int i=0;i<n;++i){
        scanf("%d",&a[i]);
        sum[1<<i]=a[i];
    }
    scanf("%d",&k);
    for(int i=0;i<k;++i)
        scanf("%d",&id[i]);
    for(int i=0;i<n;++i)
        dp[1<<i]=(a[i]!=id[0]&&a[i]!=id[1])?1:0;
    for(int i=1;i<(1<<n);++i){
        sum[i]=sum[i^lowbit(i)]+sum[lowbit(i)];//自小到大真子集枚举
        if(sum[i]==id[0]||sum[i]==id[1])continue;
        for(int j=i;j;j-=lowbit(j)){//枚举每一个1
            dp[i]+=dp[i^lowbit(j)];
            if(dp[i]>=mod)dp[i]-=mod;
        }
    }    
    printf("%d\n",dp[(1<<n)-1]);
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值