目录
计数类DP
整数划分
一个正整数 n 可以表示成若干个正整数之和,形如: n =n1 + n2 +...+nk,其中
n1≥n2≥…≥nk,k≥1。
我们将这样的一种表示称为正整数 n的一种划分
现在给定一个正整数n,请你求出 n 共有多少种不同的划分方法
输入格式
共一行,包含一个整数 n。
输出格式
共一行,包含一个整数,表示总划分数量。
由于答案可能很大,输出结果请对 10^9+ 7 取模
数据范围1≤n≤1000
输入样例:
5
输出样例:
7
思路1:看成是完全背包问题;【物品:待选集合1,2,3.... n ;个数:任意次;体积: n】
#include <bits/stdc++.h>
using namespace std;
const int N = 1010, MOD = 1e9+7;
int f[N];//组成的方案数
int main()
{
int n;
cin >> n;
f[0] = 1;
for(int i = 1; i <= n; i++){//枚举 n 个物品
for(int j = i; j <= n; j++){//枚举背包容量
f[j] = (f[j] + f[j-i]) % MOD;
}
}
cout << f[n];
return 0;
}
思路:总和 n 与数的个数 k; f[n][k] 表示总和 n 由 k 个数累加
#include <bits/stdc++.h>
using namespace std;
const int N = 1010, MOD = 1e9+7;
int f[N][N];// f[n][k] 表示总和 n 由 k 个数累加
// f[n][k] = f[n-1][k-1] + f[n-k][k]
int 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 res = 0;
for(int i = 1; i <= n; i++)res = (res + f[n][i]) % MOD;
cout << res;
return 0;
}
数位统计DP
计数问题
给定两个整数 a 和 b,求a和b之间的所有数字中 0~9的出现次数例如,a =1024,b =1032,则a和b之间共有 9 个数如下:
1024 1025 1026 1027 1028 1029 1030 1830 1032
其中 0 出现 10次,1 出现 10 次,2出现7次,3 出现3次等等
输入格式:
输入包含多组测试数据
每组测试数据占一行,包含两个整数 a 和 b
当读入一行为 0 0 时,表示输入终止,且该行不作处理
输出格式:
每组数据输出一个结果,每个结果占一行
每个结果包含十个用空格隔开的数字,第一个数字表示 0 出现的次数,第二个数字表示1 出现的次数,以此类推。
数据范围:0<a,b<100000000
输入样例:
1 10 44 497 346 542 1199 1748 1496 1403 1004 503 1714 190 1317 854 1976 494 1001 1960 0 0
输出样例:
1 2 1 1 1 1 1 1 1 1 85 185 185 185 190 96 96 96 95 93 40 40 40 93 136 82 40 40 40 40 115 666 215 215 214 205 205 154 105 106 16 113 19 20 114 20 20 19 19 16 107 105 100 101 101 197 200 200 200 200 413 1133 503 503 503 502 502 417 402 412 196 512 186 104 87 93 97 97 142 196 398 1375 398 398 405 499 499 495 488 471 294 1256 296 296 296 296 287 286 286 247
1~n,x= 1;
n =abcdefg 分别求出x在每一位出现的次数
例如:求x在第4位出现的次数1<= xxx1yyy <= abcdefg
1)xxx =000~abc-1,yyy=000~999, abc*1000
2)xxx =abc
d < 1: abc1yyy > abc0efg, 0
d = 1: yyy=0~efg, efg+1
d >1: yyy=0~999,1000
#include <bits/stdc++.h>
using namespace std;
//在数组 num 中 [r, l] 的组成的数字
int get(vector<int> num, int l, int r){
int res = 0;
for(int i = l; i >= r; i--)res = res * 10 + num[i];
return res;
}
// 10 的 x 次方
int power10(int x){
int res = 1;
while(x--){
res *= 10;
}
return res;
}
int count(int x, int k){
if(!x)return 0;// 数字 0
vector<int> alls;//存数字 x 每一位的数字 (从个位开始存)
while(x){
alls.push_back(x % 10);
x /= 10;
}
int res = 0, n = alls.size();
// !k 减去前导 0 的出现
for(int i = n - 1 - !k; i >= 0; i--){
if(i < n - 1){// [n-1, r + 1]组成的数字(xxx =000~abc-1)
res += get(alls, n-1, i+1) * power10(i);
if(!k)res -= power10(i);//k == 0 第一位不能为0
}
if(alls[i] == k)res += get(alls, i-1, 0) + 1;// d = k
else if(alls[i] > k) res += power10(i);//d > k
}
return res;
}
int main()
{
int a, b;
while(cin >> a >> b, a){
if(a > b)swap(a, b);
for(int i = 0; i <= 9; i++){//类似差分
int t = count(b, i) - count(a - 1, i);
cout << t << ' ';
}
cout << '\n';
}
return 0;
}
状态压缩DP
蒙德里安的梦想
求把 N X M 的棋盘分割成若干个 1x2的长方形,有多少种方案
例如当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
思路:核心:先放横着的,再放竖的。总方案数=只放横着的小方块的合法方案数。
如何判断方案是否合法??所有剩余位置,能否填充竖着的方块,按列来看,每一列内部所有连续的空着的小方块,需要偶数个。
dp[i][j] 表示已将前 i - 1 列摆好,且从第 i -1列,伸出到 第 i 列的所有方案(状态是 j )
第 i 列的状态,怎么转到第 k 列的状态:1)两列不能在同一列( j & k) = = 0; 2)所有连续空着的位置的长度必须是偶数。
结果:一共m列,前 m - 1 列能摆好,并且第 m - 1 列伸到第 m列的所有方案为0
#include <bits/stdc++.h>
using namespace std;
const int N = 12, M = 1 << N;
int n, m;
long long f[N][M];// f[i][j] 表示 第 i 列下 的状态 j
bool st[M];//存储每种状态是否有 奇数个连续 0
vector<int> state[M];
int main()
{
while(cin >> n >> m, n || m){
for(int i = 0; i < 1 << n; i++){// n 种状态
st[i] = true;
int cnt = 0;// 记录一列中 0 的个数
for(int j = 0; j < n; j++){//遍历这一行
if(i >> j & 1){//该状态的 第 j 位
if(cnt & 1){//这一列中 0 的个数 为 奇数
st[i] = false;break;
}
}else {
cnt++;//不放置方格
}
}
if(cnt & 1)st[i] = false; //奇数, 不合法
}
//如果不用state[M][]数组可略,这里通过判断第 i-2列 伸出和 第 i-1列伸出是否冲突进行优化
for(int j = 0; j < (1 << n); j++){ // 第 j 列的所有状态
state[j].clear();
for(int k = 0; k < 1 << n; k++){//第 k 列的所有状态
// j 和 k 不在同一列
// st[j | k] : j | k 不存在连续奇数个零,即每个格子只能用纵向的格子来填;
if(!(j & k) && st[j | k]){
state[j].push_back(k);
}
}
}
memset(f, 0, sizeof f);//初始化
f[0][0] = 1;
for(int i = 1; i <= m; i++){// i 列
for(int j = 0; j < 1 << n; j++){// 每种状态
for(auto k: state[j])
// for(int k = 0; k < 1 << n; k++){// f[i-1][k] -> f[i][j]
// j 和 k 不在同一列
if((j & k) == 0 && st[j | k]){
f[i][j] += f[i-1][k];//当前列的方案 = 之前的 i - 1 列所有状态 k 的累加
}
}
}
}
cout << f[m][0] << '\n';
}
return 0;
}
最短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输入样例:
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
思路:纯暴力解法:首尾固定尾 0 和 n - 1,中间有多种组合,用dfs 递归 复杂度会 大于 (20-2)!
动态规划:1)哪些点被用过; 2)目前停在哪个点上 2^20 * 20 = 2 * 10 ^ 7
dp[state][ j ]:表示当前的状态state,最后停在节点 j 上
dp [state][ j ] = dp [ state_k][k] + a[k][ j ]; state_k 是state除掉 j 之后的集合
#include <bits/stdc++.h>
using namespace std;
const int N = 25, M = 1 << 20;
int a[N][N], dp[M][N];// dp[i][j] 表示 状态 i 下 到到 j 的最短Hamilton路径
int main()
{
int n;
cin >> n;
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++)cin >> a[i][j];
}
memset(dp, 0x3f, sizeof dp);
dp[1][0] = 0;
for(int i = 0; i < 1 << n; i++){//状态
for(int j = 0; j < n; j++){//到达点
if(i >> j & 1){ // 当前状态中第 j 为 1
for(int k = 0; k < n; k++){
if(i - (1 << j) >> k & 1){//状态中 j 为 1 且 k 也为 1
dp[i][j] = min(dp[i][j], dp[i-(1 << j)][k] + a[k][j]);
}
}
}
}
}
cout << dp[(1 << n)-1][n-1];
return 0;
}
树形DP
没有上司的舞会
Ural 大学有 V 名职员,编号为 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
思路:创建一个树状图,用 f[n][0] 表示当前节点不选的最大快乐指数,f[n][1]表示当前节点选后的最大快乐指数。 选了当前节点子节点不能选,未选当前节点,子节点可选/可不选。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n;
int h[N];//快乐指数
vector<int> fn[N];//建树
int f[N][2];//最大的快乐指数
bool fa[N];
void dfs(int u){
f[u][1] = h[u];//当前节点要选
for(int i = 0; i < fn[u].size(); i++){
int j = fn[u][i];
dfs(j);
f[u][0] += max(f[j][0], f[j][1]);//u 不选,则它的子节点 j 可选可不选
f[u][1] += f[j][0];//u选, 子节点必不选
}
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++)cin >> h[i];//快乐指数
for(int i = 0; i < n - 1; i++){
int l, k;
cin >> l >> k;
fn[k].push_back(l);//加边
fa[l] = true;//标记有父节点
}
int root = 1;//根节点无父节点
while(fa[root])root++;
dfs(root);//从根节点开始
cout << max(f[root][0], f[root][1]);
return 0;
}
记忆化搜索
滑雪
给定一个 R 行 C 列的矩阵,表示一个矩形网格滑雪场。
矩阵中第行第列的点表示滑雪场的第行第列区域的高度
一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离
当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度
下面给出一个矩阵作为例子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
思路:遍历每个区域,从每个区域 用DFS走,返回能走到的最长滑雪长度。
f [x][y] : 表示这个区域能走到的最长滑雪长度。
#include <bits/stdc++.h>
using namespace std;
const int N = 310;
int r, c, t;
int a[N][N], f[N][N];
int dir[][2] = {{1,0},{0,1},{-1,0},{0,-1}};
int way(int x, int y){
int& v = f[x][y];
if(v != -1)return v;//未访问过
v = 1;//刚开始在第一个区域
for(int i = 0; i < 4; i++){//四个方向
int tx = x + dir[i][0];
int ty = y + dir[i][1];
//范围内,且往低处走
if(tx > 0 && tx <= r && ty > 0 && ty <= c && a[tx][ty] < a[x][y]){
v = max(v, way(tx, ty)+1);
}
}
return v;
}
int main()
{
cin >> r >> c;
for(int i = 1; i <= r; i++){
for(int j= 1; j <= c; j++)cin >> a[i][j];
}
int mx = 0;
memset(f, -1, sizeof f);//初始化
for(int i = 1; i <= r; i++){
for(int j = 1; j <= c; j++){
mx = max(mx, way(i, j));
}
}
cout << mx;
return 0;
}
本篇关于基础动态规划的计数类DP,状态压缩DP,树形DP,记忆化搜索DP。
"Talk is cheap. Show me the code."