• 【算法基础】第五章:动态规划


    Chapter 5 动态规划

    DP:

    【1】状态表示:需要几个维度来表示状态

    (1)集合(所有选法,带有约束条件

    (2)属性(最大值、最小值、数量)

    【2】状态计算:如何算出每一步的状态,对应集合的划分(不重+不漏)

    **DP的优化:**对代码/计算方程做变形

    1:背包问题

    https://www.cnblogs.com/littlehb/p/15366309.html

    01背包:每件物品最多只用一次

    条件:

    【1】只从前i个物品当中选

    【2】总体积小于等于j

    状态计算方式:含i的情况+不含i的情况
    f ( i , j ) = m a x ( f ( i − 1 , j ) , f ( i − 1 , j − v i ) + w j ) f(i,j)=max(f(i-1,j),f(i-1,j-v_i)+w_j) f(i,j)=max(f(i1,j),f(i1,jvi)+wj)

    # include 
    # include 
    # include  
    # include  
    # include 
    # include 
    # include 
    using namespace std;
    
    const int N=1010;
    
    int n,m;
    int v[N], w[N];
    int f[N][N];
    
    int main() {
    	cin>>n>>m;
    	
    	for(int i=1;i<=n;i++){
    		cin>>v[i]>>w[i];
    	}
    	
    	for(int i=1;i<=n;i++){
    		for(int j=0;j<=m;j++){
    			f[i][j] = f[i-1][j];
    			if(j >= v[i]){
    				f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]);
    			}
    		}
    	}
    	
    	cout<<f[n][m]<<endl;
        return 0;
    }
    /*
    4 5
    1 2
    2 4
    3 4
    4 5
    
    
    8
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    dp优化:转化成1维数组

    【1】f(i)只用到了f(i-1),因此可用滚动数组

    【2】j和j-vi都是小于等于j的,在j的一侧而不是两侧,因此可用1维数组

    # include 
    # include 
    # include  
    # include  
    # include 
    # include 
    # include 
    using namespace std;
    
    const int N=1010;
    
    int n,m;
    int v[N], w[N];
    int f[N]; // 从 f[N][N] 优化而来 
    
    int main() {
    	cin>>n>>m;
    	
    	for(int i=1;i<=n;i++){
    		cin>>v[i]>>w[i];
    	}
    	
    	for(int i=1;i<=n;i++){
    		// 需要从 m 遍历到 v【i】
    		// 
    		for(int j=m; j >= v[i];j--){
    			// f[i][j] = f[i-1][j]; 
    			// 优化之后是f[j] = f[j]; 直接省略 
    			f[j] = max(f[j], f[j-v[i]]+w[i]);
    		}
    	}
    	
    	cout<<f[m]<<endl;
        return 0;
    }
    /*
    4 5
    1 2
    2 4
    3 4
    4 5
    
    
    8
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    完全背包:每件物品可用无限次

    状态计算方式:
    f ( i , j ) = m a x ( f ( i − 1 , j ) , f ( i − 1 , j − v i ∗ k ) + w j ∗ k ) f(i,j)=max(f(i-1,j),f(i-1,j-v_i*k)+w_j*k) f(i,j)=max(f(i1,j),f(i1,jvik)+wjk)

    k ∗ v i < = j k*v_i<=j kvi<=j

    # include 
    # include 
    # include  
    # include  
    # include 
    # include 
    # include 
    using namespace std;
    
    const int N=1010;
    
    int n,m;
    int v[N], w[N];
    int f[N][N];
    
    int main() {
    	cin>>n>>m;
    	
    	for(int i=1;i<=n;i++){
    		cin>>v[i]>>w[i];
    	}
    	
    	for(int i=1;i<=n;i++){
    		for(int j=0;j<=m;j++){
    			for(int k=0;k*v[i]<=j;k++){
    				f[i][j] = max(f[i][j], f[i-1][j-v[i]*k] + w[i]*k);
    			}
    		}
    	}
    	
    	cout<<f[n][m]<<endl;
        return 0;
    }
    /*
    4 5
    1 2
    2 4
    3 4
    4 5
    
    
    10
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    dp优化:转化成2维数组

    【1】将f(i,j)的k次max操作展开
    f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v i ] + w i , f [ i − 1 , j − 2 v i ] + 2 w i , f [ i − 1 , j − 3 v i ] + 3 w i , . . . ) f[i,j]=max(f[i-1,j],f[i-1,j-v_i]+w_i,f[i-1,j-2v_i]+2w_i,f[i-1,j-3v_i]+3w_i,...) f[i,j]=max(f[i1,j],f[i1,jvi]+wi,f[i1,j2vi]+2wi,f[i1,j3vi]+3wi,...)
    【2】f(i,j-v)的k次max操作展开
    f [ i , j − v ] = m a x ( f [ i − 1 , j − v i ] , f [ i − 1 , j − 2 v i ] + w i , f [ i − 1 , j − 3 v i ] + 2 w i , . . . ) f[i,j-v]=max(f[i-1,j-v_i],f[i-1,j-2v_i]+w_i,f[i-1,j-3v_i]+2w_i,...) f[i,jv]=max(f[i1,jvi],f[i1,j2vi]+wi,f[i1,j3vi]+2wi,...)
    【3】分析可得,f(i,j)的状态转移可以变成是f(i-1,j)+w的结果
    f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i ] [ j − v i ] + w i ) f[i,j]=max(f[i-1,j],f[i][j-v_i]+w_i) f[i,j]=max(f[i1,j],f[i][jvi]+wi)
    合并之后可得:

    # include 
    # include 
    # include  
    # include  
    # include 
    # include 
    # include 
    using namespace std;
    
    const int N=1010;
    
    int n,m;
    int v[N], w[N];
    int f[N]; // 优化 
    
    int main() {
    	cin>>n>>m;
    	
    	for(int i=1;i<=n;i++){
    		cin>>v[i]>>w[i];
    	}
    	
    	for(int i=1;i<=n;i++){
    		for(int j=v[i];j<=m;j++){
    			f[j] = max(f[j], f[j-v[i]] + w[i]);
    		}
    	}
    	
    	cout<<f[m]<<endl;
        return 0;
    }
    /*
    4 5
    1 2
    2 4
    3 4
    4 5
    
    
    10
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    多重背包:每个物品个数不一样,不是无限,也不是只有一个,有ki的个数限制

    状态转移方程:
    f ( i , j ) = m a x ( f ( i − 1 , j ) , f ( i − 1 , j − v i ∗ k ) + w j ∗ k ) , k 从 0 到 s i f(i,j)=max(f(i-1,j),f(i-1,j-v_i*k)+w_j*k),k从0到s_i f(i,j)=max(f(i1,j),f(i1,jvik)+wjk)k0si

    # include 
    # include 
    # include  
    # include  
    # include 
    # include 
    # include 
    using namespace std;
    
    const int N=1010;
    
    int n,m;
    int v[N], w[N];
    int f[N][N];
    
    int main() {
    	cin>>n>>m;
    	
    	for(int i=1;i<=n;i++){
    		cin>>v[i]>>w[i];
    	}
    	
    	for(int i=1;i<=n;i++){
    		for(int j=0;j<=m;j++){
    			for(int k=0;k<=s[i] && k*v[i]<=j ;k++){
    				f[i][j] = max(f[i][j], f[i-1][j-v[i]*k] + w[i]*k);
    			}
    		}
    	}
    	
    	cout<<f[n][m]<<endl;
        return 0;
    }
    /*
    会超出时间限制!
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    dp优化:打包物体(二进制拆分)

    # include 
    # include 
    # include  
    # include  
    # include 
    # include 
    # include 
    using namespace std;
    
    const int N=25000, M=2010;
    
    int n,m;
    int v[N], w[N];
    int f[N]; // 优化 
    
    int main() {
    	cin>>n>>m;
    	
    	int cnt=0;
    	for(int i=1;i<=n;i++){
    		int a,b,s;
    		cin>>a>>b>>s;
    		int k=1;
    		while(k<=s){
    			cnt++;
    			v[cnt]=a*k;
    			w[cnt]=b*k;
    			s-=k;
    			k*=2;
    		}
    		if(s>0){
    			cnt++;
    			v[cnt]=a*s;
    			w[cnt]=b*s;
    		}
    	}
    	
    	n = cnt;
    	
    	for(int i=1;i<=n;i++){
    		for(int j=m;j>=v[i];j--){
    			f[j] = max(f[j], f[j-v[i]] + w[i]);
    		}
    	}
    	
    	cout<<f[m]<<endl;
        return 0;
    }
    /*
    4 5
    1 2 3
    2 4 1
    3 4 3
    4 5 2
    
    10
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    分组背包:N组,每组里面若干个,每组里面最多使用一个物品,比如水果组选择了苹果,就不能选香蕉

    # include 
    # include 
    # include  
    # include  
    # include 
    # include 
    # include 
    using namespace std;
    
    const int N=25000, M=2010;
    
    int n,m;
    int v[N][N], w[N][N], s[N];
    int f[N]; // 优化 
    
    int main() {
    	cin>>n>>m;
    	
    	for(int i=1;i<=n;i++){
    		cin>>s[i];
    		for(int j=0;j<s[i];j++){
    			cin>>v[i][j]>>w[i][j];
    		}
    	}
    	
    	for(int i=1;i<=n;i++){
    		for(int j=m;j>=0;j--){
    			for(int k=0;k<s[i];k++){
    				if(v[i][k]<=j){
    					f[j] = max(f[j],f[j-v[i][k]] + w[i][k]);
    				}
    			}
    		}
    	} 
    	
    	cout<<f[m]<<endl;
        return 0;
    }
    /*
    3 5
    2
    1 2
    2 4
    1
    3 4
    1
    4 5
    
    8
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    2:线性DP

    题目:数字三角形

    # include 
    # include 
    # include  
    # include  
    # include 
    # include 
    # include 
    using namespace std;
    
    const int N=510,INF=1e9;
    
    int n;
    int a[N][N];
    int f[N][N];
    
    int main() {
    	cin>>n;
    	
    	for(int i=1;i<=n;i++){
    		for(int j=1;j<=i;j++){
    			cin>>a[i][j];
    		}
    	}
    	
    	for(int i=0;i<=n;i++){
    		for(int j=0;j<=i;j++){
    			f[i][j] = -INF;
    		}
    	}
    	
    	f[1][1]=a[1][1];
    	for(int i=2;i<=n;i++){
    		for(int j=1;j<=i;j++){
    			f[i][j]=max(f[i-1][j-1] + a[i][j], f[i-1][j] + a[i][j]);
    		}
    	}
    	
    	int res=-INF;
    	for(int i=1;i<=n;i++){
    		res=max(res,f[n][i]);
    	}
    	cout<<res<<endl;
        return 0;
    }
    /*
    5
    7
    3 8
    8 1 0
    2 7 4 4
    4 5 2 6 5
    
    30
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    题目:最长上升子序列

    # include 
    # include 
    # include  
    # include  
    # include 
    # include 
    # include 
    using namespace std;
    
    const int N=1010;
    
    int n;
    int a[N];
    int f[N];
    
    int main() {
    	cin>>n;
    	
    	for(int i=1;i<=n;i++){
    		cin>>a[i];
    	}
    	
    	for(int i=1;i<=n;i++){
    		f[i]=1; // 只有a[i]一个数 
    		for(int j=1;j<i;j++){
    			if(a[j]<a[i]){
    				f[i]=max(f[i],f[j]+1);
    			}
    		}
    	}
    	
    	int res=0;
    	for(int i=1;i<=n;i++){
    		res=max(res,f[i]);
    	}
    	cout<<res<<endl;
        return 0;
    }
    /*
    7
    3 1 2 1 8 5 6
    
    4
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    存储状态转移过程

    # include 
    # include 
    # include  
    # include  
    # include 
    # include 
    # include 
    using namespace std;
    
    const int N=1010;
    
    int n;
    int a[N];
    int f[N], g[N];
    
    int main() {
    	cin>>n;
    	
    	for(int i=1;i<=n;i++){
    		cin>>a[i];
    	}
    	
    	for(int i=1;i<=n;i++){
    		f[i]=1; // 只有a[i]一个数 
    		g[i]=0;
    		for(int j=1;j<i;j++){
    			if(a[j]<a[i]){
    				if(f[i] < f[j]+1){
    					f[i] = f[j]+1;
    					g[i] = j;
    				}
    			}
    		}
    	}
    	
    	int k=1;
    	for(int i=1;i<=n;i++){
    		if(f[k] < f[i]){
    			k=i;
    		}
    	}
    	cout<<f[k]<<endl;
    	
    	for(int i=0,len=f[k];i<len;i++){
    		cout<<a[k]<<" ";//输出迭代序列 
    		k=g[k];
    	}
        return 0;
    }
    /*
    7
    3 1 2 1 8 5 6
    
    4
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    题目:最长公共子序列

    # include 
    # include 
    # include  
    # include  
    # include 
    # include 
    # include 
    using namespace std;
    
    const int N=1010;
    
    int n, m;
    int a[N], b[N];
    int f[N][N];
    
    int main() {
    	cin>>n>>m;
    	
    	scanf("%s%s",a+1,b+1);
    	
    	for (int i = 1; i <= n; i++){
    		for (int j = 1; j <= m; j++) {
                f[i][j] = max(f[i - 1][j], f[i][j - 1]);
                if (a[i] == b[j]){
                	f[i][j] = f[i - 1][j - 1] + 1;
    			}
            }
    	}
    	
    	cout<<f[n][m];
        return 0;
    }
    /*
    4 5
    acbd
    abedc
    
    3
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    3:区间DP

    题目:石子合并

     # include 
    # include 
    # include  
    # include  
    # include 
    # include 
    # include 
    using namespace std;
    
    const int N=310;
    
    int n, m;
    int s[N];
    int f[N][N];
    
    int main() {
    	cin>>n;
    	
    	for (int i = 1; i <= n; i++){
    		cin>>s[i];
    	}
    	
    	for (int i = 1; i <= n; i++){
    		s[i] += s[i-1];
    	}//前缀和 
    	
    	for(int len=2;len<=n;len++){
    		for(int i=1;i+len-1<=n;i++){
    			int l=i;
    			int r=i+len-1;
    			f[l][r] = 1e8; //初始化!非常重要
    			for(int k=l;k<r;k++){
    				f[l][r] = min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1]);
    			} 
    		}
    	}
    	
    	cout<<f[1][n];
        return 0;
    }
    /*
    4 
    1 3 5 2
    
    22
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    4:计数类DP

    题目:整数划分

    一个正整数 n 可以表示成若干个正整数之和,n=n1+n2+…+nk

    现在给定一个正整数 n,请求出 n 共有多少种不同的划分方法

    【二维】

    把整数n看成一个容量为n的背包,有n种物品,物品的体积分别是1~n。

    我们要求的是 恰好 装满背包的方案数(计数),每种物品 可以用无限次,所以可以看成是一个完全背包。

    状态表示

    f[i,j]:从前i中选,总和是j的选法,值就是方案数量。

    • i个物品选择了0个
      表达式:f[i−1,j] :在前i个物品中选择,结果现在第i个物品选择了0个,就是说这j个体积都是前i−1贡献的。
    • i个物品选择了s个
      f[i−1,j−s∗i]

    初值

    求最大值时,当都不选时,价值是 0。

    求方案数时,当都不选时,方案数是 1。

    #include 
    
    using namespace std;
    
    const int N = 1010;
    const int MOD = 1e9 + 7;
    int n;
    int f[N][N];
    
    int main() {
        cin >> n;
        for (int i = 1; i <= n; i++) f[i][0] = 1;
    
        for (int i = 1; i <= n; i++)       // 枚举每个物品,物品的序号与物品的体积是相等的,都是i
            for (int j = 1; j <= n; j++) { // 枚举背包容量j
                if (j >= i)                // ① 背包容量j>=当前体积i,可以选择当前数字
                    f[i][j] = (f[i][j - i] + f[i - 1][j]) % MOD;
                else
                    f[i][j] = f[i - 1][j] % MOD; // ② 放弃当前数字
            }
    
        cout << f[n][n] << endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    【一维】

    #include 
    
    using namespace std;
    
    const int N = 1010;
    const int MOD = 1e9 + 7;
    int f[N];
    int n;
    
    int main() {
        cin >> n;
        f[0] = 1; // 容量为0时,前 i 个物品全不选也是一种方案
        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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    5:数位统计DP

    题目:计数问题

    给定两个整数 a 和 b,求 a 和 b 之间的所有数字中 0∼9 的出现次数。

    #include 
    
    using namespace std;
    const int N = 32; // 2^{32}足够int用了
    int a[N], al;     // 数位分离拆开用的数组
    int f[N][N];      // 第一维:第几位数;第二维:走到当前数位,已经取得了多少个
    int n;            // 当前枚举到的是哪个数
    
    /**
     u      :从高到低,现在是第几位数
     lead   :是否考虑前导零
     st     :到当前深度已经出现n的个数
     op     :是否贴上界
     返回值 :从当前数位u出发,在当前lead,st,op的前提下,可以得到多少个符合题意的数字
    */
    int dfs(int u, int lead, int st, int op) {
        if (!u) return st;                              // 递归出口,u==0时,所有数位计算完毕,al是从1开始计数的
        if (!lead && !op && ~f[u][st]) return f[u][st]; // 非前导0 + 不贴上界 + 算过
    
        // u位上的最大值
        int up = op ? a[u] : 9; // 如果贴上界,则到op,否则可以全部取到
    
        int res = 0; // 按上面三个条件lead,st,op走到u这个数位时,最终可以获取到多少个数呢?
        for (int i = 0; i <= up; i++) {
            int sum = st;
            // ① 前面出现过非0数字 或者 本位置非0
            // ② 当前数位是要查找的数字
            if ((!lead || i > 0) && i == n) sum++;
            // 如果原来是贴上界,现在继续贴上界,那么贴上界继续
            res += dfs(u - 1, lead && !i, sum, op && i == a[u]);
        }
    
        // 记忆化
        if (!lead && !op) f[u][st] = res;
        return res;
    }
    
    int calc(int x) {
        al = 0;
        while (x) a[++al] = x % 10, x /= 10; // 高位在右,低位在左
        // al    :从al位开始
        // lead  :存在前导0
        // st    :前面填的数中数字n的个数是0个
        // op    :贴上界
        return dfs(al, 1, 0, 1);
    }
    
    int main() {
        int l, r;
        while (cin >> l >> r, l + r) {    // l+r用的漂亮,只有两个都是0时,l+r才能是0,等同于 l || r
            if (l > r) swap(l, r);        // 谁大谁小还不一定,这题真变态
            for (n = 0; n <= 9; n++) {    // 0,1,2,3,...个数都有多少个
                memset(f, -1, sizeof(f)); // 每轮需要初始化dp数组
                cout << calc(r) - calc(l - 1) << ' ';
            }
            cout << endl;
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    6:状态压缩DP

    题目:蒙德里安的梦想

    先放横的,再放竖的

    当前摆完横着的小方块之后,剩余的位置,如果能用竖着的小方块 填满 就合法,否则不合法

    如果每一列所有连续空着的小方块个数存在奇数个,必然填充不满(不合法情况)

    状态表示
    f [ i ] [ j ] :已经摆完前 i 列,且第 i 列的状态为 j 的所有方案 f[i][j]:已经摆完前i列,且第i列的状态为j的所有方案 f[i][j]:已经摆完前i列,且第i列的状态为j的所有方案
    f【0】【i】中,除了f【0】【0】都是非法的,所以赋值为0

    最终结果

    已经摆完第m列,并且第m列的状态为0,也就是第m列没有任何一个小方格新去摆放横条

    #include 
    
    using namespace std;
    typedef long long LL;
    const int N = 12;
    const int M = 1 << N;
    int n, m;
    
    LL f[N][M];       // 已经摆完前i列并且第i列的状态为j的所有方案
    vector<int> v[M]; // 对于每个状态而言,能转转移到它的状态有哪些,预处理一下(二维数组)
    int ok[M];        // 某种状态是否合法,就是是不是存在奇数个连续0
    
    int main() {
        while (cin >> n >> m, n + m) {
            // ① 预处理:枚举行数n的每个二进制位,可以枚举出每种可能出现的状态对应的二进制表示,这些状态有些是不合法的
            // 只有不存在连续奇数个数字0的状态才是合法的,一旦n确定了,这个有效状态是可以预处理出来的
            for (int i = 0; i < 1 << n; i++) {
                int cnt = 0;                // 连续0的个数
                ok[i] = 1;                  // 初始设置此状态是合法的
                for (int j = 0; j < n; j++) // 遍历此状态的每一个二进制位,开始检查
                    if (i >> j & 1) {       // 如果本位是1,表示连续0发生中断,需要统计连续0的个数,并且记得清空cnt
                        if (cnt & 1) {      // 奇数个连续0, (cnt & 1) = (cnt % 2 >0)
                            ok[i] = 0;      // 连续0发生中断,此状态为不合法状态
                            break;          // 不用再往后看了,一次失足就不挽救
                        }
                        cnt = 0; // 连续个零计数重新开始
                    } else
                        cnt++; // 连续0的计数器++
    
                // 最后一个cnt++后,依然可能有连续奇数个,举个栗子:n=4=(0100)_2,完成数位枚举后,cnt=1,也就是高位存在奇数个0
                if (cnt & 1) ok[i] = 0;
            }
            // ② 预处理:枚举每个状态,获取可能是从哪些有效状态转移过来
            for (int i = 0; i < 1 << n; i++) {
                // 多组数据,每次预处理时清空一下
                // vector v[M] 是一个二维数组,初始化比较麻烦,需要用循环遍历第一维,然后再v[i].clear()进行清空
                v[i].clear();
    
                // 状态i,从哪些状态转化而来?
                for (int j = 0; j < 1 << n; j++) // j为前序状态
                    // (1) i & j==0 同一行不能同时探出小方格,那样会有重叠
                    // (2) 解释一下ok[i | j]
                    // 比如: 01000 | 10101 = 11101,描述当前完成状态叠加后的最终状态,在预处理的数组中找一下,是不是合法状态
                    if ((i & j) == 0 && ok[i | j]) v[i].push_back(j);
            }
    
            // 多组数据,每次清零
            memset(f, 0, sizeof f);
            // 初始方案数
            f[0][0] = 1; // 可以理解为 从虚拟的第0开始(第一个0),还没有向右探出小方格(第二个0),此时的方案数只有1种。
            // 如果你想为什么不是0种,下面的递推关系就会让你明白,0做为基底,就啥也递推不出来了。
    
            // DP正式开始
            for (int i = 1; i <= m; i++)
                for (int j = 0; j < 1 << n; j++) // 遍历第i列的所有状态j
                    for (auto k : v[j])          // 遍历第i-1列的所有状态k
                        f[i][j] += f[i - 1][k];  // 每个合法状态,均需从它的前序有效状态转移而来
            // 输出答案
            cout << f[m][0] << endl;
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    题目:最短Hamilton路径

    #include 
    
    using namespace std;
    const int N = 20;     // 好小的上限N,大的没法状态压缩实现,2^N不能太大啊!
    const int M = 1 << N; // 2的N次方
    int w[N][N];          // 邻接矩阵,记录每两个点之间的距离
    int f[M][N];          // DP状态数组,记录每一步的最优解
    int n;                // n个结点
    
    int main() {
        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);
    
        // ① 初始化,从0出发到0结束,路线状态表示为1
        f[1][0] = 0; // 从0走到0,路线为1,也就是二进制表示法为(1)_2,表示0出现过
    
        for (int i = 0; i < (1 << n); i++)      // 枚举所有路线
            for (int j = 0; j < n; j++)         // 枚举每个节点作为阶段性终点
                if (i >> j & 1) {               // 这个节点是不是包含在路径中
                    for (int k = 0; k < n; k++) // 引入结点k,使得距离更短
                        // 需要满足i这个路径中除去j这个点,一定要包含k这个点
                        if ((i - (1 << j)) >> k & 1)
                            f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
                }
    
        // 最终经历了所有结点,并且最后停在n-1(最后一个点,因为坐标从0开始)这个点
        cout << f[(1 << n) - 1][n - 1] << endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    7:树形DP

    题目:没有上司的舞会

    f [ u , 0 ] :所有以 u 为根的子树中选,并且不选 u 这个点的方案 f[u,0]:所有以u为根的子树中选,并且不选u这个点的方案 f[u,0]:所有以u为根的子树中选,并且不选u这个点的方案

    f [ u , 1 ] :所有以 u 为根的子树中选,并且选 u 这个点的方案 f[u,1]:所有以u为根的子树中选,并且选u这个点的方案 f[u,1]:所有以u为根的子树中选,并且选u这个点的方案

    状态转移
    f [ u , 0 ] = ∑ m a x ( f ( s o n , 0 ) , f ( s o n , 1 ) ) f[u,0]=∑max(f(son,0),f(son,1)) f[u,0]=max(f(son,0),f(son,1))

    f [ u , 1 ] = ∑ f ( s o n , 0 ) f[u,1]=∑f(son,0) f[u,1]=f(son,0)

    #include 
    using namespace std;
    const int N = 6010;
    
    int happy[N]; // 快乐值
    int in[N];    // 入度
    int f[N][2];  // dp的状态结果数组
    int n;
    
    // 构建邻接表
    int h[N], e[N], ne[N], idx;
    void add(int a, int b) {
        e[idx] = b, ne[idx] = h[a], h[a] = idx++;
    }
    
    // 通过深度优先搜索,对树进行遍历
    void dfs(int u) {
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            dfs(v);                           // 继续探索它的孩子,它的值是由它的孩子来决定的
            f[u][1] += f[v][0];               // 它选择了,它的孩子就不能再选
            f[u][0] += max(f[v][0], f[v][1]); // 它不选择,那么它的每一个孩子,都是可以选择或者不选择的
        }
        // 不管是不是叶子结点,都会产生happy[u]的价值
        f[u][1] += happy[u];
    }
    
    int main() {
        // 邻接表表头初始化
        memset(h, -1, sizeof h);
    
        cin >> n;
        for (int i = 1; i <= n; i++) cin >> happy[i];
    
        // 读入树
        for (int i = 1; i < n; i++) { // n-1条边
            int x, y;
            cin >> x >> y;
            add(y, x);
            in[x]++; // 记录入度,因为需要找出大boss
        }
    
        // 从1开始找根结点
        int root = 1;
        while (in[root]) root++; // 找到根结点,入度为0
    
        // 递归
        dfs(root);
    
        // 取两个
        printf("%d\n", max(f[root][0], f[root][1]));
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    8:记忆化搜索

    递归求解DP问题——记忆化搜索

    题目:滑雪

    分4个方向讨论:上下左右

    #include 
    
    using namespace std;
    const int N = 310;
    int dx[] = {-1, 0, 1, 0};
    int dy[] = {0, 1, 0, -1};
    int g[N][N]; // 邻接矩阵,地图
    int f[N][N]; // 记录从i到j的最大距离
    int n, m;    // 行与列
    int res;     // 结果
    
    // 记忆化搜索
    int dfs(int x, int y) {
        if (~f[x][y]) return f[x][y];
        // 求最长的轨迹,最起码是1
        f[x][y] = 1;
        for (int i = 0; i < 4; i++) {
            int tx = x + dx[i], ty = y + dy[i];
            if (tx < 1 || tx > n || ty < 1 || ty > m) continue;
            if (g[x][y] > g[tx][ty])
                f[x][y] = max(f[x][y], dfs(tx, ty) + 1);
        }
        return f[x][y];
    }
    
    int main() {
        cin >> n >> m; // n行 m列
        // 读入每一个点的高度
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= m; j++)
                cin >> g[i][j];
        // 初始化-1
        memset(f, -1, sizeof f);
    
        // 从每一个点出发
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= m; j++)
                res = max(res, dfs(i, j));
        // 输出结果
        printf("%d\n", res);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
  • 相关阅读:
    二十六、rosbag功能包
    SQL基础知识笔记
    Groovy系列一 Groovy基础语法
    ThinkPHP6 输出二维码图片格式 解决与 Debug 的冲突
    Vue绑定style和class 对象写法
    Github每日精选(第41期):跨端数据同步加密工具restic
    罗德施瓦兹频谱仪使用笔记
    行驶证识别易语言代码
    五子棋对战简单介绍
    2023年4月到7月工作经历
  • 原文地址:https://blog.csdn.net/m0_65787507/article/details/138161251