当前位置:   article > 正文

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

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

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 <iostream>
# include <string.h>
# include <algorithm> 
# include <stdio.h> 
# include <vector>
# include <queue>
# include <unordered_set>
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 <iostream>
# include <string.h>
# include <algorithm> 
# include <stdio.h> 
# include <vector>
# include <queue>
# include <unordered_set>
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 <iostream>
# include <string.h>
# include <algorithm> 
# include <stdio.h> 
# include <vector>
# include <queue>
# include <unordered_set>
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 <iostream>
# include <string.h>
# include <algorithm> 
# include <stdio.h> 
# include <vector>
# include <queue>
# include <unordered_set>
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 <iostream>
# include <string.h>
# include <algorithm> 
# include <stdio.h> 
# include <vector>
# include <queue>
# include <unordered_set>
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 <iostream>
# include <string.h>
# include <algorithm> 
# include <stdio.h> 
# include <vector>
# include <queue>
# include <unordered_set>
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 <iostream>
# include <string.h>
# include <algorithm> 
# include <stdio.h> 
# include <vector>
# include <queue>
# include <unordered_set>
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 <iostream>
# include <string.h>
# include <algorithm> 
# include <stdio.h> 
# include <vector>
# include <queue>
# include <unordered_set>
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 <iostream>
# include <string.h>
# include <algorithm> 
# include <stdio.h> 
# include <vector>
# include <queue>
# include <unordered_set>
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 <iostream>
# include <string.h>
# include <algorithm> 
# include <stdio.h> 
# include <vector>
# include <queue>
# include <unordered_set>
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 <iostream>
# include <string.h>
# include <algorithm> 
# include <stdio.h> 
# include <vector>
# include <queue>
# include <unordered_set>
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 <iostream>
# include <string.h>
# include <algorithm> 
# include <stdio.h> 
# include <vector>
# include <queue>
# include <unordered_set>
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 <bits/stdc++.h>

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 <bits/stdc++.h>

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 <bits/stdc++.h>

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 <bits/stdc++.h>

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<int> 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 <bits/stdc++.h>

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 <bits/stdc++.h>
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 <bits/stdc++.h>

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
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/484200
推荐阅读
相关标签
  

闽ICP备14008679号