动态规划(三)

计数类DP

900.整数划分

在这里插入图片描述
在这里插入图片描述

  • 根据题意,将其归于完全背包问题,即从1~i中选择任意多的数,其总和为j的所有选法
  • 根据最后一步选择多少i求出状态计算递推公式为f[i,j]=f[i-1,j]+f[i,j-i]
  • 根据滚动数组优化为f[j]=f[j]+f[j-i]
//二维写法
#include<iostream>

using namespace std;

const int N=1010,mod=1e9+7;

int f[N][N];

int main(){
    
    int n;
    
    cin>>n;
    
    f[0][0]=1;                //从前0个数选择总和为0,即什么都不选,应当算一种方案
    
    for(int i=1;i<=n;i++)
       for(int j=0;j<=n;j++){                             //选择总和为0也应当计入方案内
           
           f[i][j]=f[i-1][j];                             //一个i都不选
           
           if(j>=i)f[i][j]=(f[i][j]+f[i][j-i])%mod;      //至少j要能容纳一个i
       }
       
    cout<<f[n][n];
}
//一维优化
#include<iostream>

using namespace std;

const int N=1010,mod=1e9+7;

int f[N];

int main(){
    
    int n;
    
    cin>>n;
    
    f[0]=1;
    
    for(int i=1;i<=n;i++)
       for(int j=i;j<=n;j++)
           
          f[j]=(f[j]+f[j-i])%mod;
       
       
    cout<<f[n];
}

思路二

在这里插入图片描述

  • 状态转移方程为f[i,j]=f[i-1,j-1]+f[i-j,j]
  • 最后答案枚举从f[n,1]+...f[n,n]的和
#include<iostream>

using namespace std;

const int N=1010,mod=1e9+7;

int f[N][N];

int main(){
    
    int n;
    
    cin>>n;
    
    f[0][0]=1;
    
    for(int i=1;i<=n;i++)
       for(int j=1;j<=i;j++){
           
           f[i][j]=(f[i-1][j-1]+f[i-j][j])%mod;
       }
       
    int res=0;
    
    for(int i=1;i<=n;i++)res=(res+f[n][i])%mod;
    
    cout<<res;
}

数位统计DP

338.计数问题

在这里插入图片描述

  • 我们实现一个函数count(n,x),用于计算1~n 所有数字中,x(0~9)的出现次数
  • 例:求1在1<=xxx1yyy<=abcdefg这个区间内第四位出现的次数,分情况讨论
  • 1.xxx∈[0,abc-1]此时xxx1yyy一定属于该区间,则yyy=000~999,总共有abc*1000种情况
  • 2.xxx=abc
  • d<1(d=0),此时abc1yyy一定大于abc0efg,0种情况
  • d=1,此时yyy可取000~efg,总共efg+1种情况
  • d>1,此时yyy可取000~999,总共1000种情况
  • 那么求解1在a~b之间出现的次数即可用count(b,1)-count(a-1,1)表示
#include<iostream>
#include<algorithm>
#include<vector>

using namespace std;

int gets(vector<int> nums,int l,int r){      //用于统计低位l到高位r之间的数字大小
    
    int res=0;
    
    for(int i=r;i>=l;i--){
        
        res=res*10+nums[i];
    }
    
    return res;
}

int pow10(int x){                     //用于计算10的x次方
    
    int res=1;
    
    while(x--)res*=10;
    
    return res;
}

int count(int n,int x){              //用于计算1~n 所有数字中,x的出现次数
    
    if(!n)return 0;                 //n=0不合法,返回0
     
    vector<int> nums;               //nums用于按位存储数字n
    
    while(n){
        
        nums.push_back(n%10);
        
        n/=10;
    }
    
    n=nums.size();
    
    int res=0;
    
    for(int i=n-1-!x;i>=0;i--){               //枚举x可能出现的位置,从最高位开始枚举,如果x是0,则不可能出现在最高位,从次高位开始枚举
         
        if(i<n-1){                           //x不在最高位的情况
            
            res+=gets(nums,i+1,n-1)*pow10(i);
            
            if(!x)res-=pow10(i);             //如果x=0,则高位应当是001~abc,故减掉一个10^i
        }
        
        if(nums[i]==x)res+=gets(nums,0,i-1)+1;    //枚举接下来两种情况
        
        else if(nums[i]>x)res+=pow10(i);
    }
    
    return res;
}

int main(){
    
    int a,b;
    
    while(cin>>a>>b , a||b){
        
        if(a>b)swap(a,b);
        
        for(int i=0;i<10;i++){
            
            cout<<count(b,i)-count(a-1,i)<<" ";
        }
        
        cout<<endl;
    }
}

状态压缩DP

291.蒙德里安的梦想

在这里插入图片描述

  • 解题思想:假设所有横置的小方块都已经放好,那么列置的小方块只要按顺序依次填空即可(空必须可以填满),那么只要枚举所有横置的小方块成立的情况即可
  • 横置的小方块都是1行*2列的,所以横着填一次会填满两格,我们观察第i列的所有情况,用f[i,j]表示第i列中,有j行被上一列填入的小方格占据(j是二进制,假设j最大是5行,那么被占据第五行就是00001,被占据第4,5行就是00011...以此类推)
  • 考虑到i-1列同样被i-2列伸出的小方格占据,即我们考虑f[i-1,k]f[i,j]的状态是否会在第i列产生冲突,如果k&j==0即不会在第i列产生冲突
  • 同样我们考虑横格放好之后,竖格是否可以正确插入的情况,由于竖方块是2行*1列的,假如出现了竖行奇数空行,如3*1的空格,那么则无法正确插入,即出现连续的奇数空格(st[j|k]),是不符合条件的答案(j|k如j=10010,k=00001,j|k=10011,那么第k列存在两个空位)
  • 我们用st数组存放所有竖格不满足条件的情况,例如一共有五行,那么用二进制表示的00011即是一种不满足条件的情况,而00011对应10进制是3,所以st[3]应当存储false,满足条件的存储true

#include<iostream>
#include<cstring>

using namespace std;

const int N=12,M=1<<N;

int st[M];

long long f[N][M];                            //N是列数,M对应行的最大二进制数

int main(){
    
    int n,m;                                 //n行m列
    
    while(cin>>n>>m && n||m){
        
        memset(f,0,sizeof f);
        
        //初始化st数组
        
        for(int i=0;i<1<<n;i++){                //枚举每一个二进制数
            
            st[i]=true;                         //默认i列是可行方案
            
            int cnt=0;                          //计数连续的0
            
            for(int k=0;k<n;k++){
                
                if(i>>k&1){                     //i当前最后一位是否为1,如果是1,说明连续的0中断
                    
                    if(cnt&1)st[i]=false;       //如果cnt是奇数,那么当前列的方案不可行
                    
                    cnt=0;
                }
                
                else cnt++;
            }
            
            if(cnt&1)st[i]=false;               //判断最后一组连续的0是否为奇数
        }
        
        //dp过程,枚举0~m列状态转移过程
        f[0][0]=1;                              //第0列没有上一列伸出的方块,所以上一列转移来的只有一种情况
        
        for(int i=1;i<=m;i++)
        
           for(int j=0;j<1<<n;j++)
           
              for(int k=0;k<1<<n;k++)
              
              if( (k&j)== 0 && st[k|j])f[i][j]+=f[i-1][k];   //如果可以正确转移,加上前一列所有的方案数
              
        cout<<f[m][0]<<endl;        //当m+1列是00000若干个0时(没有方块伸出),是合法答案,输出结果
        
    }
}

最短Hamilton路径

在这里插入图片描述
在这里插入图片描述

  • i是n位二进制,其中0代表没有经过该点,1表示经过了该点,例如i=100001表示经过第0和第5个点
  • 状态计算中以倒数第二个点经过哪一个点来划分状态,若i-(1<<j)>>k&1==1即k与j不重合,并且i的k位为1(表示经过k点),则更新状态f[i][j]=f[i-(1<<j)][k]+w[k][j],并在若干k中取最小值
  • 此题注意'-'号优先度高于'<<'和'>>'
#include<iostream>
#include<cstring>

using namespace std;

const int N=20,M=1<<N;

int f[M][N];     //状态表示

int w[N][N];     //带权图

int main(){
    
    int n;
    
    cin>>n;
    
    for(int i=0; i<n; i++)
       for(int j=0; j<n; j++) 
       cin>>w[i][j];
       
    memset(f,0x3f,sizeof f);
     
    f[1][0]=0;                  //从0走到0并且经过第0个点的路径为长度为0
    
    for(int i=0; i<1<<n; i++)
       for(int j=0; j<n; j++)
            if(i>>j&1){                    //枚举的i应当包含终点j
               for(int k=0;k<n;k++)
                   if(i-(1<<j)>>k&1)       //枚举的i应当包含倒数第二个点k并且不能与j重合
                   f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j]);
            }
        
    cout<<f[(1<<n)-1][n-1];               //输出经过所有点且终点是n-1的路径大小
}

树形DP

285.没有上司的舞会

在这里插入图片描述
在这里插入图片描述

#include<iostream>
#include<cstring>

using namespace std;

const int N=6010;

int n;

int f[N][2];

int happy[N];                             //存放快乐指数

int h[N],e[N],ne[N],idx;

bool has_father[N];                       //判断谁是根节点,根节点没有父节点

void add(int a,int b){
    
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}

void dfs(int u){
    
    f[u][1]=happy[u];                    //选择u的情况初值是happy[u]
    
    for(int i=h[u];i!=-1;i=ne[i]){       //dfs所有儿子节点
        
        int j=e[i];
        
        dfs(j);                          
        
        f[u][1]+=f[j][0];               //分别处理选不选u的两种情况
        
        f[u][0]+=max(f[j][0],f[j][1]);
        
    }
}

int main(){
    
    cin>>n;
    
    for(int i=1;i<=n;i++)cin>>happy[i];
    
    memset(h,-1,sizeof h);

    for(int i=0;i<n-1;i++){
        
        int a,b;
        
        cin>>a>>b;
        
        add(b,a);              //b是a的父节点
        
        has_father[a]=true;    //表示a存在父节点
    }
    
    int root=1;
    
    while(has_father[root])root++;    //寻找根节点
    
    dfs(root);
    
    cout<<f[root][0]<<"  "<<f[root][1]<<endl;
    
    cout<<max(f[root][0],f[root][1]);
    
    return 0;
}

记忆化搜索

901.滑雪

在这里插入图片描述
在这里插入图片描述

  • 使用记忆化数组 记录每个点的最大滑动长度
    遍历每个点作为起点
    然后检测该点四周的点 如果可以滑动到其他的点
    那么该点的最大滑动长度 就是其他可以滑到的点的滑动长度+1
  • dp[i][j] = max(dp[i][j-1], dp[i][j+1],dp[i-1][j],dp[i+1][j])

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

const int N=310;

int map[N][N];

int r,c;

int f[N][N];

int dx[4]={1,0,-1,0} ,dy[4]={0,1,0,-1};      

int dp(int i,int j){
    
    int &v=f[i][j];           //v代替f[i][j]
    
    if(v!=-1)return v;        //记忆化的好处
    
    v=1;                      //初始路径为1
    
    for(int k=0;k<4;k++){
        
        int a=i+dx[k],b=j+dy[k];
        
        if(a>=0 && b>=0 && a<r && b<c && map[a][b]<map[i][j])v=max(v,dp(a,b)+1);
    }
    
    return v;
}

int main(){
    
    cin>>r>>c;

    for(int i=0;i<r;i++)
        for(int j=0;j<c;j++)
        cin>>map[i][j];
        
    memset(f,-1,sizeof f);          //没有枚举过的点初值为-1
    
    int res=0;
    
    for(int i=0;i<r;i++)
       for(int j=0;j<c;j++)
       res=max(res,dp(i,j));        //dp每一个点,找寻最大值
          
    cout<<res;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值