1)计数类DP:
900. 整数划分
一个正整数 n
可以表示成若干个正整数之和,形如:n=n1+n2+…+nk,其中 n1≥n2≥…≥nk,k≥1
。
我们将这样的一种表示称为正整数 n
的一种划分。
现在给定一个正整数 n
,请你求出 n
共有多少种不同的划分方法。
输入格式
共一行,包含一个整数 n
。
输出格式
共一行,包含一个整数,表示总划分数量。
由于答案可能很大,输出结果请对 109+7
取模。
数据范围
1≤n≤1000
输入样例:
5
输出样例:
7
* 可以将此题理解为完全背包问题来解救;
* 状态表示:dp[i][j] : 表示只选择前i个数,和为j的组合方案有多少;
*
* 因为 dp[i][j] = dp[i-1][j] + dp[i-1][j-i] +
* dp[i-1][j-i*2] +……+ )
*
* 又因为 dp[i][j-i] = dp[i-1][j-i] + dp[i-1][j-2*i] +
* dp[i-1][j-3*i] + …… + ;
* 所以 dp[i][j] = dp[i-1][j],dp[i][j-i] ;
*
* 观摩动态转移方程,我们会发现:
* dp[i-1][j] , dp[i][j-i] 一定先于 dp[i][j] 计算出来,因此我们就可以
* 完整的,正确的得到这个解,但是在此之前,我们也得看一下第一个数该初始化
* 哪些数据才能使之正确;
* 第一个数据是 dp[1][0] , 那么想必初始化dp[0][0] = 1 即可;意味着我选择
* 前0个数的和是0,我选择0个数有0种选择,我一个数都不选择有一种方案,
* 所以 dp[0][0] = 1即是正确的;(但是这种只初始化一个数据时,需要j从0开始
* 枚举;)
* 我们可以把选择前k个数,和为0的方案的值全部初始化为1,由上分析,这显然是
* 正确的;
*
* 从评论区学到一种初始化的方法,当初始化的值由题目的定义不好得出直接答案时,
* 从当前的状态转移方程和题目定义的第一个状态的值 去倒推初始化数据的值,
* 而使得这个初始化的值能够保证状态转移方程是正确的。
- /**
- * 可以将此题理解为完全背包问题来解救;
- * 状态表示:dp[i][j] : 表示只选择前i个数,和为j的组合方案有多少;
- *
- * 因为 dp[i][j] = dp[i-1][j] + dp[i-1][j-i] +
- * dp[i-1][j-i*2] +……+ )
- *
- * 又因为 dp[i][j-i] = dp[i-1][j-i] + dp[i-1][j-2*i] +
- * dp[i-1][j-3*i] + …… + ;
- * 所以 dp[i][j] = dp[i-1][j],dp[i][j-i] ;
- *
- * 观摩动态转移方程,我们会发现:
- * dp[i-1][j] , dp[i][j-i] 一定先于 dp[i][j] 计算出来,因此我们就可以
- * 完整的,正确的得到这个解,但是在此之前,我们也得看一下第一个数该初始化
- * 哪些数据才能使之正确;
- * 第一个数据是 dp[1][0] , 那么想必初始化dp[0][0] = 1 即可;意味着我选择
- * 前0个数的和是0,我选择0个数有0种选择,我一个数都不选择有一种方案,
- * 所以 dp[0][0] = 1即是正确的;(但是这种只初始化一个数据时,需要j从0开始
- * 枚举;)
- * 我们可以把选择前k个数,和为0的方案的值全部初始化为1,由上分析,这显然是
- * 正确的;
- *
- * 从评论区学到一种初始化的方法,当初始化的值由题目的定义不好得出直接答案时,
- * 从当前的状态转移方程和题目定义的第一个状态的值 去倒推初始化数据的值,
- * 而使得这个初始化的值能够保证状态转移方程是正确的。
- */
-
- #include <iostream>
- #include <algorithm>
-
- using namespace std;
-
- const int maxn = 1010 , mod = 1e9+7;
- int dp[maxn][maxn];
-
- int main()
- {
- int n;
- cin >> n;
-
- dp[0][0]=1;
-
- for(int i=1;i<=n;++i)
- for(int j=0;j<=n;++j)
- {
- if(j<i)
- dp[i][j] = dp[i-1][j];
- else
- dp[i][j] = (dp[i-1][j] + dp[i][j-i])%mod;
- }
-
- cout << dp[n][n] << endl;
- return 0;
- }
-
-
//滚动数组优化
- //滚动数组优化
-
- #include <iostream>
- #include <algorithm>
-
- using namespace std;
-
- const int maxn = 1010 , mod = 1e9+7;
- int dp[maxn];
-
- int main()
- {
- int n;
- cin >> n;
-
- dp[0]=1;
-
- for(int i=1;i<=n;++i)
- for(int j=0;j<=n;++j)
- {
- if(j<i)
- dp[j] = dp[j];
- else
- dp[j] = (dp[j] + dp[j-i])%mod;
- }
-
- cout << dp[n] << endl;
- return 0;
- }
* dp[i][j] 表示选择了i个数,和为j的方案总数;
* 状态转移方程:dp[i][j] = dp[i-1][j-1] + dp[i][j-i] ;
* (最小值等于1的数的方案,或者最小值大于1的数的方案);
* 1)把最小值1去掉,选择的数目个数为i-1,和为j-1,dp[i][j] = dp[i-1][j-1];
* 2)把每个值都减去一,由于最小值是大于1的,所以减去之后最小值最小也是1,
* 所以 dp[i][j] = dp[i][j-i];
* 把 1)和2)的结果加起来即为 dp[i][j]的值;
- /**
- * dp[i][j] 表示选择了i个数,和为j的方案总数;
- * 状态转移方程:dp[i][j] = dp[i-1][j-1] + dp[i][j-i] ;
- * (最小值等于1的数的方案,或者最小值大于1的数的方案);
- * 1)把最小值1去掉,选择的数目个数为i-1,和为j-1,dp[i][j] = dp[i-1][j-1];
- * 2)把每个值都减去一,由于最小值是大于1的,所以减去之后最小值最小也是1,
- * 所以 dp[i][j] = dp[i][j-i];
- * 把 1)和2)的结果加起来即为 dp[i][j]的值;
- */
-
- #include <iostream>
- #include <algorithm>
-
- using namespace std;
-
- const int maxn = 1010 , mod = 1e9+7;
- int dp[maxn][maxn];
-
- int main()
- {
- int n;
- cin >> n;
-
- for(int i=0;i<=n;++i)
- dp[1][i] = 1; //选择1个数,和为i的方案必定只有一种
-
- for(int i=1;i<=n;++i)
- for(int j=i;j<=n;++j)
- {
- if(j == i) //if语句不需要也行,因为这句已经已经包含在else语句中了
- dp[i][j] = 1 ; //选择i个数,和为i的方案也必定只有一种
- else
- dp[i][j] = (dp[i-1][j-1] + dp[i][j-i])%mod;
- }
-
- int res = 0; //需要累加一下
- for(int i=1;i<=n;++i)
- res = (res+dp[i][n]) % mod;
-
- cout << res << endl;
- return 0;
- }
2)树形DP:
285. 没有上司的舞会
Ural 大学有 N
名职员,编号为 1∼N
。
他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。
每个职员有一个快乐指数,用整数 Hi
给出,其中 1≤i≤N
。
现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。
在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。
输入格式
第一行一个整数 N
。
接下来 N
行,第 i 行表示 i 号职员的快乐指数 Hi
。
接下来 N−1
行,每行输入一对整数 L,K,表示 K 是 L
的直接上司。
输出格式
输出最大的快乐指数。
数据范围
1≤N≤6000
,
−128≤Hi≤127
输入样例:
- 7
- 1
- 1
- 1
- 1
- 1
- 1
- 1
- 1 3
- 2 3
- 6 4
- 7 4
- 4 5
- 3 5
输出样例:
5
* 树形 dp,每个节点有选与不选两种方案,所以我们可以用dp[maxn][2]用来做
* 状态表示:
* dp[u][0]:以u为根节点的子树的最大快乐指数,并且不能选择u;
* dp[u][1]:以u为根节点的子树的最大快乐指数,并且可以选择u;
*
* 不选择u这个节点,那么我们就可以既选择u的孩子节点,也可以不选择u的孩子
* 节点;
* 选择u这个结点,那么我们肯定是不能选择u的孩子节点的。
*
* 状态计算:
* dp[u][0] = dp[u][0] + max(dp[v][1],dp[v][0]);
* dp[u][1] = max( dp[u][1] , (dp[u][1] + dp[v][0]) );
* 但只要我们能保证dp[v][0]非负,我们就可以将 dp[u][1]简化为:
* dp[u][1] = dp[u][1]+dp[v][0];
* 由dp的定义,我们一定能确定dp[v][0]非负,最糟糕的情况莫过于所有节点
* 的快乐值都为负数,我所有节点都不选,那么dp[v][0]=0;
*
* dp[u][0] = dp[u][0] + max(dp[v][1],dp[v][0]);
* 你说为什么要把dp[u][0]的值也要累加给dp[u][0],即为什么不能是:
* dp[u][0] = max(dp[v][1],dp[v][0]);
* 这是因为u有可能由多个孩子节点,根据题意,如果以孩子节点为根的子树的
* 最大快乐值都是正数,那么肯定得把所有子树都加上,即在孩子节点v之前已经
* 有孩子结点j的值加到dp[u][0] 上了,那在考虑孩子节点v的时候,是肯定得加上
* dp[u][0] 的值。
* 同理,dp[u][1]一样。
- /**
- * 树形 dp,每个节点有选与不选两种方案,所以我们可以用dp[maxn][2]用来做
- * 状态表示:
- * dp[u][0]:以u为根节点的子树的最大快乐指数,并且不能选择u;
- * dp[u][1]:以u为根节点的子树的最大快乐指数,并且可以选择u;
- *
- * 不选择u这个节点,那么我们就可以既选择u的孩子节点,也可以不选择u的孩子
- * 节点;
- * 选择u这个结点,那么我们肯定是不能选择u的孩子节点的。
- *
- * 状态计算:
- * dp[u][0] = dp[u][0] + max(dp[v][1],dp[v][0]);
- * dp[u][1] = max( dp[u][1] , (dp[u][1] + dp[v][0]) );
- * 但只要我们能保证dp[v][0]非负,我们就可以将 dp[u][1]简化为:
- * dp[u][1] = dp[u][1]+dp[v][0];
- * 由dp的定义,我们一定能确定dp[v][0]非负,最糟糕的情况莫过于所有节点
- * 的快乐值都为负数,我所有节点都不选,那么dp[v][0]=0;
- *
- * dp[u][0] = dp[u][0] + max(dp[v][1],dp[v][0]);
- * 你说为什么要把dp[u][0]的值也要累加给dp[u][0],即为什么不能是:
- * dp[u][0] = max(dp[v][1],dp[v][0]);
- * 这是因为u有可能由多个孩子节点,根据题意,如果以孩子节点为根的子树的
- * 最大快乐值都是正数,那么肯定得把所有子树都加上,即在孩子节点v之前已经
- * 有孩子结点j的值加到dp[u][0] 上了,那在考虑孩子节点v的时候,是肯定得加上
- * dp[u][0] 的值。
- * 同理,dp[u][1]一样。
- *
- */
-
- #include <iostream>
- #include <algorithm>
-
- using namespace std;
-
- const int maxn = 6010;
- vector<int> Adj[maxn];
- bool hs[maxn]; //确定根节点
- int c[maxn]; //结点的快乐值
- int dp[maxn][2]; //状态表示
-
- void dfs(int u)
- {
- dp[u][1] = c[u]; //起初,以u为根的子树只有u这一个结点
-
-
- for(int i=0;i<Adj[u].size();++i)
- {
- int v = Adj[u][i];
- dfs(v);
-
- dp[u][0] = max(dp[v][0] , dp[v][1]);
- //dp[u][1] += dp[v][0]; //这个和下面一个都可
- dp[u][1] = max(dp[u][1] , dp[u][1]+dp[v][0]);
- }
- }
-
- int main()
- {
- int n;
- cin >> n;
-
- for(int i=1;i<=n;++i)
- cin >> c[i];
-
- for(int i=1;i<n;++i)
- {
- int u,v;
- cin >> v >> u;
- Adj[u].push_back(v);
- hs[v] = 1;
- }
-
- int root = 1;
- while(hs[root])
- ++root;
-
- dfs(root);
-
- cout << max(dp[root][0] , dp[root][1]) << endl;
-
-
- return 0;
- }
3)记忆化搜索:
901. 滑雪
给定一个 R
行 C
列的矩阵,表示一个矩形网格滑雪场。
矩阵中第 i
行第 j 列的点表示滑雪场的第 i 行第 j
列区域的高度。
一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。
当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。
下面给出一个矩阵作为例子:
- 1 2 3 4 5
-
- 16 17 18 19 6
-
- 15 24 25 20 7
-
- 14 23 22 21 8
-
- 13 12 11 10 9
在给定矩阵中,一条可行的滑行轨迹为 24−17−2−1
。
在给定矩阵中,最长的滑行轨迹为 25−24−23−…−3−2−1
,沿途共经过 25
个区域。
现在给定你一个二维矩阵表示滑雪场各区域的高度,请你找出在该滑雪场中能够完成的最长滑雪轨迹,并输出其长度(可经过最大区域数)。
输入格式
第一行包含两个整数 R
和 C
。
接下来 R
行,每行包含 C
个整数,表示完整的二维矩阵。
输出格式
输出一个整数,表示可完成的最长滑雪长度。
数据范围
1≤R,C≤300
,
0≤矩阵中整数≤10000
输入样例:
- 5 5
- 1 2 3 4 5
- 16 17 18 19 6
- 15 24 25 20 7
- 14 23 22 21 8
- 13 12 11 10 9
输出样例:
25
* 状态表示:dp[i][j] : 从(i,j)这个点开始滑离,能滑的最远距离;
* 状态计算:
* 从上来:dp[i,j] = max(dp[i,j] , dp[i-1,j]+1);
* 从下来:dp[i,j] = max(dp[i,j] , dp[i+1,j]+1);
* 从左来:dp[i,j] = max(dp[i,j] , dp[i,j-1]+1);
* 从右来:dp[i,j] = max(dp[i,j] , dp[i,j+1]+1);
- /**
- * 状态表示:dp[i][j] : 从(i,j)这个点开始滑离,能滑的最远距离;
- * 状态计算:
- * 从上来:dp[i,j] = max(dp[i,j] , dp[i-1,j]+1);
- * 从下来:dp[i,j] = max(dp[i,j] , dp[i+1,j]+1);
- * 从左来:dp[i,j] = max(dp[i,j] , dp[i,j-1]+1);
- * 从右来:dp[i,j] = max(dp[i,j] , dp[i,j+1]+1);
- */
-
- #include <iostream>
- #include <algorithm>
-
- using namespace std;
-
- const int maxn = 310;
- int g[maxn][maxn]; //地图
- int dp[maxn][maxn]; //状态表示
- int n,m;
-
- int dx[]={0,1,0,-1}; //坐标偏移量
- int dy[]={1,0,-1,0};
-
- int dfs(int x,int y)
- {
- int &v = dp[x][y];
- if(v!=0) return v; //如果dp[x][y]已经计算过,直接返回其值;
-
- v=1;
- for(int i=0;i<4;++i)
- {
- int a = x+dx[i],b = y+dy[i];
- if(a>=1 && a<=n && b>=1 && b<=m && g[a][b] < g[x][y])
- v = max(v,dfs(a,b)+1); //如果从(a,b) 到(x,y)能使dp[x][y]变大
- }
- return v;
- }
-
- int main()
- {
- cin >> n >> m;
-
- for(int i=1;i<=n;++i)
- for(int j=1;j<=m;++j)
- cin >> g[i][j];
-
- int res = 0;
- for(int i=1;i<=n;++i)
- for(int j=1;j<=m;++j)
- res = max(res,dfs(i,j));
-
- cout << res << endl;
- return 0;
- }
4)状态压缩DP:
91. 最短Hamilton路径
给定一张 n
个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1
的最短 Hamilton 路径。
Hamilton 路径的定义是从 0
到 n−1
不重不漏地经过每个点恰好一次。
输入格式
第一行输入整数 n
。
接下来 n
行每行 n 个整数,其中第 i 行第 j 个整数表示点 i 到 j 的距离(记为 a[i,j]
)。
对于任意的 x,y,z
,数据保证 a[x,x]=0,a[x,y]=a[y,x] 并且 a[x,y]+a[y,z]≥a[x,z]
。
输出格式
输出一个整数,表示最短 Hamilton 路径的长度。
数据范围
1≤n≤20
0≤a[i,j]≤107
输入样例:
- 5
- 0 2 4 5 1
- 2 0 6 5 3
- 4 6 0 8 3
- 5 5 8 0 5
- 1 3 3 5 0
输出样例:
18
* 状态压缩dp:将某一维的值用二进制数来表示,将用二进制表示的值看作是若干
* 种状态,每一位上的值表示一种状态,可根据具体题目来设定,例如,可以将每
* 一位上二进制值为1表示某个点已经在这个集合中,反之,值为0表示这个点没有
* 在集合中。
*
* 对于这个题目:
* 状态设计:dp[i][j]:从起点到j,中间途径i用二进制表示的位为1的点的路径距离
* 最小值;
*
* 状态转移方程:
* 1)中途不途径k:dp[i][j] = dp[i][j];
* 2)中途从k转移过来:
* dp[i][j] = min(dp[i][j],dp[i - (1<
*
* 初始化:dp[1][0] = 0;从起点到起点不需要距离;
* 答案:dp[(1<
- /**
- * 状态压缩dp:将某一维的值用二进制数来表示,将用二进制表示的值看作是若干
- * 种状态,每一位上的值表示一种状态,可根据具体题目来设定,例如,可以将每
- * 一位上二进制值为1表示某个点已经在这个集合中,反之,值为0表示这个点没有
- * 在集合中。
- *
- * 对于这个题目:
- * 状态设计:dp[i][j]:从起点到j,中间途径i用二进制表示的位为1的点的路径距离
- * 最小值;
- *
- * 状态转移方程:
- * 1)中途不途径k:dp[i][j] = dp[i][j];
- * 2)中途从k转移过来:
- * dp[i][j] = min(dp[i][j],dp[i - (1<
- * 选不选择从k到j,还是得比较一下的;
- *
- * 初始化:dp[1][0] = 0;从起点到起点不需要距离;
- * 答案:dp[(1<
- * j等于n-1;
- */
-
- #include <iostream>
- #include <algorithm>
-
- using namespace std;
-
- const int N = 20,M=1<<N;
- int w[N][N]; //距离
- int dp[M][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];
-
- fill(*dp,*dp+N*M,M);
- dp[1][0]=0; //初始化,从起点到起点不需要距离;
-
- for(int i=1;i < 1<<n ; ++i) //枚举一个点,两个点,...,n个点
- for(int j=0;j<n;++j) //这种路径所走过的点j
- if(i>>j & 1)
- {
- for(int k=0;k<n;++k)//这种路径是否能从k到j使得起点到j的距离变小
- if(i>>k & 1)
- dp[i][j] = min(dp[i][j],dp[i - (1<<j)][k]+w[k][j]);
- }
-
- //i用二进制表示从低到高位有连续的n个1,j等于n-1;
- cout << dp[(1<<n) -1][n-1] << endl;
- return 0;
- }
5)数位统计DP:
338. 计数问题
给定两个整数 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
。
当读入一行为 0 0 时,表示输入终止,且该行不作处理。
输出格式
每组数据输出一个结果,每个结果占一行。
每个结果包含十个用空格隔开的数字,第一个数字表示 0 出现的次数,第二个数字表示 1 出现的次数,以此类推。
数据范围