整数划分(计数类DP)
分析
该问题可以转化为完全背包问题,只不过是恰好装满背包
怎么体现恰好为j?
- f[i][j]的含义是前i个数字中选,和恰好为j的方案数
- 集合划分,不重不漏的划分出所有情况即f[i][j]由哪些状态转移过来
- 根据1、2两步可以确定恰好为j,因为我们的含义和集合划分保障了正确性
代码一(朴素版)
不同的初始化条件会使for循环中的下标起始位置不同
f[0][0] = 1;//0个数字和为0的方案为空集,空集也代表一种方案
如果仅初始化f[0][0],for循环中的j的下标就要从0开始,因为f[0][i]表示从0个数中选,和为j的方案数是空集,空集我们也看做一种方案!
#include<iostream>
using namespace std;
const int N=1010,mod = 1e9+7;
int f[N][N];//f[i][j]表示在前i个物品(数字)中选,体积恰好为j的方案数
int main()
{
int n;
cin>>n;
//边界的处理和下标的起始是密切联系的
f[0][0] = 1;//0个数字和为0的方案为空集,空集也代表一种方案
// for(int i=1;i<=n;i++) f[i][0]=1;这样初始化j的下标可以从1开始
for(int i=1;i<=n;i++){//第i个物品开始选择
for(int j=0;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];
return 0;
}
代码二(优化一)
// f[i][j] = f[i - 1][j] + f[i][j - i]
#include <iostream>
using namespace std;
const int N = 1e3 + 7, mod = 1e9 + 7;
int f[N][N];
int main() {
int n;
cin >> n;
for (int i = 0; i <= n; i ++) {
f[i][0] = 1; // 容量为0时,前 i 个物品全不选也是一种方案
}
for (int i = 1; i <= n; i ++) {
for (int j = 0; j <= n; j ++) {
f[i][j] = f[i - 1][j] % mod; // 特殊 f[0][0] = 1
if (j >= i) f[i][j] = (f[i - 1][j] + f[i][j - i]) % mod;
}
}
cout << f[n][n] << endl;
}
代码三(优化二)
// f[i][j] = f[i - 1][j] + f[i][j - i]
#include <iostream>
using namespace std;
const int N = 1e3 + 7, mod = 1e9 + 7;
int f[N];
int main() {
int n;
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;
}
走方格(计数类DP)
给定一个 n×m 的方格阵,沿着方格的边线走,从左上角 (0,0) 开始,每次只能往右或者往下走一个单位距离,问走到右下角 (n,m) 一共有多少种不同的走法。
输入格式
共一行,包含两个整数 n 和 m。
输出格式
共一行,包含一个整数,表示走法数量。
数据范围
1≤n,m≤10
输入样例:
2 3
输出样例:
10
分析
代码
在初始化时,有可能没有含义,为方便状体转移而进行初始化
或者是空集,空集也是一种方案
#include<iostream>
using namespace std;
const int N = 12;
int f[N][N];//从起点到[i,j]的方案数
int main()
{
int n,m;
cin>>n>>m;
//初始化有时候可能没有实际含义,为方便第一步的状态转移
f[0][0] = 1;//初始化,如果只有一个点(0,0)那么从(0,0)->(0,0)只有一种情况
for(int i = 0;i<=n;i++){
for(int j = 0;j<=m;j++){
if(j>=1 && i>=1){
f[i][j] = f[i-1][j]+f[i][j-1];
}else if(i>=1 && j==0){
f[i][j] = f[i-1][j];
}else if(i==0 && j>=1){
f[i][j] = f[i][j-1];
}
}
}
cout<<f[n][m];
return 0;
}
没有上司的舞会(树形DP)
分析
- 用邻接表存储树
邻接表中的每个节点只存储相邻的节点
// 对于每个点k(比如上图的0、1、2、3、4节点),开一个单链表,存储k所有可以走到的相邻的点。h[k]存储这个单链表的头结点,e[]存储数据域的值,ne[]存储结点的next指针
int h[N], e[N], ne[N];
int idx; //idx表示第几个操作,方便为节点分配空间
- 分析图
代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 6010;
int n;
int h[N],e[N],ne[N],idx;
int happy[N];
int f[N][2];
bool has_fa[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]; //加上根节点的值
for(int i = h[u];i!=-1;i=ne[i]){
int j = e[i];
dfs(j);//获得所有子树的最大值
//dfs(j)结束后返回
f[u][1] +=f[j][0];
f[u][0] +=max(f[j][0],f[j][1]);
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&happy[i]);
memset(h,-1,sizeof h);
for(int i=0;i<n-1;i++){
int a,b;
cin>>a>>b;//b是a的父节点
add(b,a);
has_fa[a] = true;//表明a有父节点
}
int root = 1;
while(has_fa[root]) root++;//通过遍历找到根节点
dfs(root);
cout<<max(f[root][0],f[root][1]);
return 0;
}
滑雪(记忆化搜索)
给定一个 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
分析
代码
递归形式
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
const int N = 310;
int g[N][N];//存储图
int f[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};//上下左右移动
int n,m;
int dp(int x,int y){
int &v = f[x][y];
if(v!=-1) return v;//说明该点已经计算过
v=1;//根据题意可知当前区域也需要算上所以是1
for(int i=0;i<=3;i++){
int a = x+dx[i],b=y+dy[i];
if(a>0 && a<=n && b>0 && b<=m && g[x][y]>g[a][b])//判断条件
v = max(v,dp(a,b)+1);
}
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];
}
}
memset(f , -1 ,sizeof f);//将数组f的初始值置为-1,表示还没有计算过
int res = 0;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
res=max(res,dp(i,j));//每个点都尝试一下
}
}
cout<<res;
return 0;
}
蒙德里安的梦想(状态压缩DP)
求把 N×M 的棋盘分割成若干个 1×2 的长方形,有多少种方案。
例如当 N=2,M=4 时,共有 5 种方案。当 N=2,M=3 时,共有 3 种方案。
如下图所示:
输入格式
输入包含多组测试用例。
每组测试用例占一行,包含两个整数 N 和 M。
当输入用例 N=0,M=0 时,表示输入终止,且该用例无需处理。
输出格式
每个测试用例输出一个结果,每个结果占一行。
数据范围
1≤N,M≤11
输入样例:
1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0
输出样例:
1
0
1
2
3
5
144
51205
分析
代码
#include<bits/stdc++.h>
using namespace std;
const int N=12, M = 1<< N;
long long f[N][M] ;// 第一维表示列, 第二维表示所有可能的状态
bool st[M]; //存储每种状态是否有奇数个连续的0,如果奇数个0是无效状态,如果是偶数个零置为true。
//vector<int > state[M]; //二维数组记录合法的状态
vector<vector<int>> state(M); //两种写法等价:二维数组
int m , n;
int main(){
while(cin>>n>>m, n||m){ //读入n和m,并且不是两个0即合法输入就继续读入
//第一部分:预处理1
//对于每种状态,先预处理每列不能有奇数个连续的0
for(int i=0; i< 1<<n; i++){
int cnt =0 ;//记录连续的0的个数
bool isValid = true; // 某种状态没有奇数个连续的0则标记为true
for(int j=0;j<n;j++){ //遍历这一列,从上到下
if( i>>j &1){ //i>>j位运算,表示i(i在此处是一种状态)的二进制数的第j位; &1为判断该位是否为1,如果为1进入if
if(cnt &1) { //这一位为1,看前面连续的0的个数,如果是奇数(cnt &1为真)则该状态不合法
isValid =false;break;
}
cnt=0; // 既然该位是1,并且前面不是奇数个0(经过上面的if判断),计数器清零。//其实清不清零没有影响
}
else cnt++; //否则的话该位还是0,则统计连续0的计数器++。
}
if(cnt &1) isValid =false; //最下面的那一段判断一下连续的0的个数
st[i] = isValid; //状态i是否有奇数个连续的0的情况,输入到数组st中
}
//第二部分:预处理2
// 经过上面每种状态 连续0的判断,已经筛掉一些状态。
//下面来看进一步的判断:看第i-2列伸出来的和第i-1列伸出去的是否冲突
for(int j=0;j< 1<<n;j++){ //对于第i列的所有状态
state[j].clear(); //清空上次操作遗留的状态,防止影响本次状态。
for(int k=0;k< 1<<n;k++){ //对于第i-1列所有状态
if((j&k )==0 && st[ j| k] ) // 第i-2列伸出来的 和第i-1列伸出来的不冲突(不在同一行)
//解释一下st[j | k]
//已经知道st[]数组表示的是这一列没有连续奇数个0的情况,
//我们要考虑的是第i-1列(第i-1列是这里的主体)中从第i-2列横插过来的,还要考虑自己这一列(i-1列)横插到第i列的
//比如 第i-2列插过来的是k=10101,第i-1列插出去到第i列的是 j =01000,
//那么合在第i-1列,到底有多少个1呢?自然想到的就是这两个操作共同的结果:两个状态或。 j | k = 01000 | 10101 = 11101
//这个 j|k 就是当前 第i-1列的到底有几个1,即哪几行是横着放格子的
state[j].push_back(k); //二维数组state[j]表示第j行,
//j表示 第i列“真正”可行的状态,如果第i-1列的状态k和j不冲突则压入state数组中的第j行。
//“真正”可行是指:既没有前后两列伸进伸出的冲突;又没有连续奇数个0。
}
}
//第三部分:dp开始
memset(f,0,sizeof f); //全部初始化为0,因为是连续读入,这里是一个清空操作。类似上面的state[j].clear()
f[0][0]=1 ;// 这里需要回忆状态表示的定义,按定义这里是:前第-1列都摆好,且从-1列到第0列伸出来的状态为0的方案数。
//首先,这里没有-1列,最少也是0列。其次,没有伸出来,即没有横着摆的。即这里第0列只有竖着摆这1种状态。
for(int i=1;i<= m;i++){ //遍历每一列:第i列合法范围是(0~m-1列)
for(int j=0; j< 1<<n; j++){ //遍历当前列(第i列)所有状态j
for( auto k : state[j]) // 遍历第i-1列的状态k,如果“真正”可行,就转移
f[i][j] += f[i-1][k]; // 当前列的方案数就等于之前的第i-1列所有状态k的累加。
}
}
//最后答案是什么呢?
//f[m][0]表示 前m-1列都处理完,并且第m-1列没有伸出来的所有方案数。
//即整个棋盘处理完的方案数
cout<< f[m][0]<<endl;
}
}
补充
对于循环中下标是从0开始还是从1开始
- 如果遇到j-1、i-1的情况下标就从1开始,否则就从0开始
- 计算方案数或者计数的情况下一般从0开始
- 且j从0还是1开始与初始化的情况有密切联系