动态规划_计数类dp_数位统计dp_状态压缩dp_树形dp_记忆化搜索

Posted 一只特立独行的猫

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划_计数类dp_数位统计dp_状态压缩dp_树形dp_记忆化搜索相关的知识,希望对你有一定的参考价值。

计数类dp

给定方案限制,统计某一种方案出现个数

原题链接

https://www.acwing.com/problem/content/902/

题目大意

一个正整数 n 可以表示成若干个正整数之和,形如:n=n1+n2+…+nk,中 n1≥n2≥…≥nk,k≥1。
我们将这样的一种表示称为正整数 n的一种划分。现在给定一个正整数 n
,请你求出 n 共有多少种不同的划分方法。
0<n<=1000

思路

通过完全背包解决。
f ( i , j ) f(i,j) f(i,j)表示前 i i i个数放入容量为 j j j的背包的组合数量。由于使用 [ 1 − ( j − 1 ) ] [1-(j-1)] [1(j1)] j j j,所以一定可以凑满。
朴素版:
状态表示: f ( i , j ) f(i,j) f(i,j)表示用1到i的数凑j的数量
状态计算: f ( i , j ) = f ( i − 1 , j ) + f ( i − 1 , j − 1 ∗ i ) + f ( i − 1 , j − 2 ∗ i ) + . . . f(i,j)=f(i-1,j)+f(i-1,j-1*i)+f(i-1,j-2*i)+... f(i,j)=f(i1,j)+f(i1,j1i)+f(i1,j2i)+...
优化版:
状态表示:同朴素版,但是用到了滚动数组优化
状态计算: f ( j ) = f ( j ) + f ( j − i ) f(j)=f(j)+f(j-i) f(j)=f(j)+f(ji)
具体的证明看这篇博客。
https://blog.csdn.net/qq_45931661/article/details/119999547
其他版
状态表示: f ( i , j ) f(i,j) f(i,j)表示用 j j j个数凑 i i i的数量
状态计: f ( i , j ) = f ( i − 1 , j − 1 ) + f ( i − j , j ) f(i,j)=f(i-1,j-1)+f(i-j,j) f(i,j)=f(i1,j1)+f(ij,j)

代码

朴素版,O(n^3)的复杂度

#include<iostream>

using namespace std;

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

int n;
int f[N][N];

int main(){
    cin>>n;
    
    for(int i=0;i<=n;i++) f[i][0]=1;
    
    for(int i=1;i<=n;i++){//枚举物品
        for(int j=1;j<=n;j++){//枚举背包容量
            for(int k=0;k*i<=j;k++){//状态计算
                f[i][j]=(f[i][j]+f[i-1][j-k*i])%mod;
            }
        }
    }
    cout<<f[n][n]<<endl;
    return 0;
}

优化版,O(n^2)的复杂度。

#include<iostream>

using namespace std;

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

int n;
int f[N];

int main(){
    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]<<endl;
    return 0;
}

其他版:

#include<iostream>

using namespace std;

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

int n;
int f[N][N];

int main(){
    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<<endl;
    return 0;
}

数位统计DP

统计数字中的一些规律,如出现数字的次数等等。

题目链接:

https://www.acwing.com/problem/content/340/

题目大意:

给定两个整数 a 和 b,求 a 和 b 之间的所有数字中 0∼9的出现次数。
例如,a=1024,b=1032,则 a 和 b 之间共有 9个数如下:
1024 1025 1026 1027 1028 1029 1030 1031 1032
其中 0 出现 10次,1 出现 10 次,2 出现 7 次,3 出现 3 次等等…

思路:

求[a,b]区间内x出现的次数,可以采用前缀和的思想, r e s = c o u n t ( b , x ) − c o u n t ( a − 1 , x ) res=count(b,x)-count(a-1,x) res=count(b,x)count(a1,x)
现在分析如何求解 c o u n t ( a , x ) count(a,x) count(a,x),即[1,a]中x出现的次数。
设要求的数为abcdefg
以分析第4位出现x的情况为例:

  1. x!=0
    1.形式: { 0 − ( a b c − 1 ) } d { 0 − 1000 } \\{0 - (abc-1)\\}d\\{0-1000\\} {0(abc1)}d{01000}
    意义:前缀小于abc的情况
    2.1.形式: a b c d { 0 − 1000 } abcd\\{0-1000\\} abcd{01000}
    意义:d==x的情况
    2.2 形式: a b c d { 0 − e f g } abcd\\{0-efg\\} abcd{0efg}
    意义:d<x的情况
    2.3 形式:不存在
    意义:d>x的情况
  2. x==0
    1.形式: { 1 − ( a b c − 1 ) } d { 0 − 1000 } \\{1 - (abc-1)\\}d\\{0-1000\\} {1(abc1)}d{01000}
    意义:前缀小于abc的情况,但是要注意0的前缀不能为0,否则就没有意义了。
    2.1.形式: a b c d { 0 − 1000 } abcd\\{0-1000\\} abcd{01000}
    意义:d==x的情况
    2.2 形式: a b c d { 0 − e f g } abcd\\{0-efg\\} abcd{0efg}
    意义:d<x的情况
    2.3 形式:不存在
    意义:d>x的情况
    枚举每一位上的x出现的次数,就可以的到最终的答案。

代码:

#include<iostream>
#include<vector>
#include<cmath>

using namespace std;

int n,m;

int get(vector<int > num, int r, int l){
	//将num的r到l位化成一个数字
    int res=0;
    for(int i=r;i>=l;i--){
        res=res*10+num[i];
    }
    return res;
}

int count(int a,int x){
    //统计1~a当中x出现的次数
    if(a==0) return 0;
    vector<int > num;//存储每一位
    while(a){
        //低下标放低位
        num.push_back(a%10);
        a/=10;
    }
    
    int n = num.size();
    long long res=0;
    for(int i=n-1-!x;i>=0;i--){
        //从高到低枚举每一位
        res+=(long long )get(num,n-1,i+1)*pow(10,i);//1情况
        if(!x) res-=pow(10,i);
        
        if(x==num[i]) res+=get(num,i-1,0)+1;//2.1情况
        else if(x<num[i]) res+=pow(10,i);//2.2情况
    }
    return res;
}

int main(){
    while(cin>>n>>m,n||m){
        if(n>m) swap(n,m);
        
        for(int i=0;i<10;i++){
            cout<<count(m,i)-count(n-1,i)<<" ";//利用前缀和的思想
        }
        cout<<endl;
    }
    return 0;
}

状态压缩DP

通过将状态用二进制位表示来实现dp的过程。

蒙德里安的梦想

原题链接:
https://www.acwing.com/problem/content/293/

题目大意:

求把 N×M 的棋盘分割成若干个 1×2的的长方形,有多少种方案。
例如当 N=2,M=4时,共有 5 种方案。当 N=2,M=3 时,共有 3种方案。
如下图所示:

思路:

1.可以发现,一旦给定横着的积木的位置,竖着的积木的位置也确定了。
2.用j的二进制来表示每一列的状态。如下图,第i列的状态就是 ( 100 ) 2 (100)_2 (100)2

**状态表示: f ( i , j ) f(i,j) f(i,j)表示第 i i i列, 状态为 j j j的填充方案个数。

状态计算: f ( i , j ) + = f ( i − 1 , k ) f(i,j)+=f(i-1,k) f(i,j)+=动态规划

6. 动态规划

BZOJ_1833_[ZJOI2010]_数字计数_(数位dp)

数位DP计数问题

XDOJ_1117_状态压缩DP

BZOJ_1833_[ZJOI2010]count 数字计数_数位DP