算法学习19:动态规划(计数类dp、状态压缩dp、树状dp、记忆化搜索)
文章目录
前言:
提示:以下是本篇文章正文内容:
一、计数类dp:
1.整数划分:
方法1:模仿“完全背包问题”:总和是固定的,个数是变动的。
// 一个正整数n可以表示为若干个正整数之和,形如:n = n1 + n2 + ... + nk,
// 其中n1 >= n2 >= ... >= nk, k > 1.
// 我们将这样的一种表示称为n的一种划分,给定一个n,求出有多少种不同的划分方法。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010, 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;// 状态方程
// 注意:对mod取余的原因是:防止结果过大
cout << f[n] << endl;
return 0;
}
方法2:总和是变动的,个数是固定的。
// 一个正整数n可以表示为若干个正整数之和,形如:n = n1 + n2 + ... + nk,
// 其中n1 >= n2 >= ... >= nk, k > 1.
// 我们将这样的一种表示称为n的一种划分,给定一个n,求出有多少种不同的划分方法。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010, 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 ans = 0;
for(int i = 1; i <= n; i ++) ans = (ans + f[n][i]) % mod;
cout << ans << endl;
return 0;
}
思考:
二、状态压缩dp
1.例题:蒙德里安的梦想:(待填坑!!!😓)
// 求把n*m的棋盘分割成若干个1*2的长方形,有多少种方案。
// 案例1: 2*4的棋盘有5种方案
// 案例2: 2*3的棋盘有3种方案
// 输入多组测试用例,每组占一行,包含整数n,m,如果n=0,m=0,表示输入终止。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 12, M = 1 << N;
int n, m;
int f[N][M];// 要摆第i列式,上一列小方格伸出来的数量为j的情况
bool st[M];// 连续的奇数个0
int main()
{
// 首先执行 cin >> n >> m,然后执行 n || m,并且循环的条件是基于 n || m 的结果。
// 都为0,就退出
while(cin >> n >> m, n || m)
{
memset(f, 0, sizeof f);
// 预处理:所有的状态 是否不存在 连续的奇数个0
// 状态:i < 1 << n
for(int i = 0; i < 1 << n; i ++)
{
st[i] = true;
int cnt = 0;// 当前这一段,连续0的个数
for(int j = 0; j < n; j ++)
if(i >> j & 1)// 找到1
{
// 判断是否有奇数个0
if(cnt & 1) st[i] = false;
cnt = 0;
}
else cnt ++;
if(cnt & 1) st[i] = false;
}
f[0][0] = 1;
for(int i = 1; i <= m; i ++)// 枚举列
for(int j = 0; j < 1 << n; j ++)// 枚举所有的状态
for(int k = 0; k < 1 << n; k ++)// // 枚举所有第i-1列的状态
if((j & k) == 0 && st[j | k])
f[i][j] += f[i - 1][k];
cout << f[m][0] << endl;
}
return 0;
}
2. 最短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] <= 10^7
哈密顿路径也称作哈密顿链,指在一个图中沿边访问每个顶点恰好一次的路径。
*/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20, M = 1 << N;
int n;
int w[N][N];// 邻接矩阵
int f[M][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);
f[1][0] = 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 ++)
if((i - (1 << j)) >> k & 1)
f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
cout << f[(1 << n) - 1][n - 1] << endl;
return 0;
}
三、树状dp:
例题:没有上司的晚会
/*
Ural大学里面有N名职员,编号为1 ~ N
它们的关系就像一颗一校长为根的数,父结点就是子节点的直接上司
每一个职员有一个 快乐指数,用整数Hi表示
现在要召开一个周年庆宴会,不过没有职员愿意和直接上司一起参加
在满足这个的条件下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。
输入:
第一行:N
接下来N行,第i行表示i号职员的快乐指数Hi
接下来N-1行,每行输入一堆整数a、b,表示b是a的直接上司
输出:最大的快乐指数
数据范围:
1 <= N <= 6000 -128 <= Hi <= 127
*/
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 6010;
int n;
int happy[N];// 快乐指数
int h[N], e[N], ne[N], idx;// 邻接表
int f[N][2];// 状态
bool has_father[N];// 索引i是否有父节点
// 将b作为a的邻接节点插入
// 由于树的原因,它就作为父节点
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);
f[u][0] += max(f[j][0], f[j][1]);
f[u][1] += f[j][0];
}
}
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;
scanf("%d%d", &a, &b);
has_father[a] = true;
add(b, a);// ??? a是指节点,是要被插入的那一个。
}
int root = 1;
while(has_father[root]) root ++;// 找到根节点,因为根节点没有 父节点
dfs(root);
printf("%d\n", max(f[root][0], f[root][1]));
return 0;
}
四、记忆化搜索:
例题:滑雪
/*
给定R行C列的矩阵,表示一个矩形网格滑雪场。
矩阵第i行第j列的点表示滑雪场的第i行第j列区域的高度。
一个人从滑雪场中的某一个区域出发,每次可以向 上下左右 任意一个方向移动一个单位的距离。
当然,前提是该区域的高度低于自己目前所在区域的高度。
现在给定你一个二维矩阵表示滑雪场各区域的高度,请你找出在该滑雪场能够完成的最长滑雪轨迹,并输出长度(可以经过最大区域数)
输入:
第一行包含R C
加下来R行,每行包含C个整数
输出:可完成的最长滑雪长度
数据范围:
1 <= R C <= 300
0 <= 矩阵中的整数 <= 10000
*/
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 310;
int n, m;
int h[N][N];// 高度
int f[N][N];// 状态
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int dp(int x, int y)
{
int &v = f[x][y];
if(v != -1) return v;
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 && h[a][b] < h[x][y])
v = max(v, dp(a, b) + 1);// (假设向右)f[i][j] = f[i][j + 1] + 1
}
return v;
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
scanf("%d", &h[i][j]);
memset(f, -1, sizeof f);
int res = 0;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
res = max(res, dp(i, j));// dp任意起点
printf("%d\n", res);
return 0;
}
// 思考:会记下来我们搜索过的地方
五、补充:为什么要对1e9 + 7取余???
😊😊😊😊引用别人的博客来解释(1)😊😊😊
😊😊😊😊引用别人的博客来解释(2)😊😊😊
总结
提示:这里对文章进行总结:
💕💕💕