第五章 动态规划三
第五章 动态规划三
数位统计dp
计数问题
给定两个整数 a 和 b,求 a 和 b 之间的所有数字中 0∼9 的出现次数。
例如,a=1024,b=1032,则 a 和 b 之间共有 9 个数如下:
1024 1025 1026 1027 1028 1029 1030 1031 1032
其中 0 出现 1010 次,1 出现 1010 次,2 出现 77 次,3 出现 33 次等等…
输入格式
输入包含多组测试数据。
每组测试数据占一行,包含两个整数 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
分析
分情况讨论:
定义函数:count(n,x),其表示在1到n中,x出现的次数(x是0-9)
那么,可以用类似前缀和的思想,来求解a到b中,x出现的次数:count(b,x) - count(a-1,x)
以x = 1为例,如何计算count(n, 1):
比如n是个7位的数字 abcdefg,我们可以分别求出1在每一位上出现的次数,然后做一个累加即可。
比如求1在第4位上出现的次数,求有多少个形如xxx1yyy的数在1到abcdefg之间
- xxx = 000 ~ abc - 1, yyy = 000 ~ 999,一共有abc * 1000种选法
- xxx = abc
- d < 1,abc1yyy > adc0efg,0种
- d = 1,yyy = 000 ~ efg,efg + 1种
- d > 1,yyy = 000 ~ 999,1000种
把上面全部的情况,累加起来,就是1出现在第四位的次数。
类似的,可以求解出1在任意一个位置上出现的次数,累加起来,就求出了1在每一位上出现的此时,即求解出了count(n,1)。
进一步,能够求解出count(n,x)
需要注意一下边界问题:当x=0时,不能有前导0,所以当x=0时,形如xxx0yyy,前面的xxx是从001到999
#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
int count(int n, int x) { // 1 - n中 x 出现的次数
int res = 0;
int len = log10(n) + 1; // 求n的位数
for (int i = 1; i <= len; i++) { // 枚举第i位(从左往右)
// abcdefg
// 假如枚举d位,p = 1000,l = abc,r = 1000,j = d
int p = pow(10, len - i), l = n / p / 10, r = n % p, j = n / p % 10;
// xxx < abc
// x == 0,xxx = 001 ~ abc - 1, yyy = p, (l - 1) * p
if (x == 0) res += (l - 1) * p;
// x != 0, xxx = 000 ~ abc - 1, yyy = p, l * p
else res += l * p;
// xxx = abc
// j == x,r + 1种
if (j == x) res += r + 1;
// j > x,p种
if (j > x) res += p;
}
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++) cout << count(b, i) - count(a - 1, i) << " ";
cout << endl;
}
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
分析
核心思路:先放横着的,再放竖着的。
总方案数,等于只放横着的小方块的合法方案数。(放完横着的方块之后,竖着的只能被动填充进去)
方案合法的条件是:当横着的方块放完后,竖着的小方块恰好能把剩余的地方全部填满。
那如何判断方案是否合法呢?即怎么看竖着的小方块是否能把剩余部分填满呢?因为是竖着放的,所以可以按列来看,每一列的内部,只要所有连续的空余小方块的个数为偶数,即可。
状态表示:我们用f[i,j]表示,已经将前i-1列摆好,且从i-1列,伸出到第i列,状态是j,的所有方案。
什么叫做,从第i-1列,伸出来到第i列呢,如下图,第i列的第1,2,5个格子,是从i-1列伸过来的。此时的状态j为 \(11001_2\),即对于第i列的所有格子,第1,2,5个格子被伸出来占据了(j是个二进制数,若该列的某一行,有伸出来,则用1表示,否则用0表示)。

状态计算:状态转换,通常是考虑最后一步的情况,根据最后一步的操作来进行分类,我们考虑f[i - 1][k],即在i-1列的所有可能状态,f[i][j]一定是由某些f[i - 1][k]转移过来的,那k需要满足什么条件,才能够从f[i - 1][k]转移到f[i][j]呢?
k需要满足的条件:
k和j不能冲突,判断是否有(k & j ) == 0- 第
i - 1列中连续的空白格子必须是偶数,(k | j)不能有奇数个连续的0
理解:其实状态计算看的是横着的摆放完之后第i列和第 i - 1 列的关系,既不能有冲突,又要使第i- 1列能够摆完整
我们对于每个状态k,可以预处理出,这个状态的二进制表示中,所有连续0的个数是否是偶数(若所有连续0的个数是偶数,则我们称该状态为合法状态),我们用一个布尔数组st[k]来记录这个信息,当st[k] = true时,表示状态k是合法的。

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 12, M = 1 << N;
int n, m;
long long f[N][M];
bool st[M];
int main() {
while (cin >> n >> m, n || m) {
// 预处理状态合法性
for (int i = 0; i < 1 << n; i++) {
st[i] = true;
int cnt = 0;
for (int j = 0; j < n; j++) {
if (i >> j & 1) {
if (cnt & 1) {
st[i] = false;
break;
}
} else cnt++;
}
if (cnt & 1) st[i] = false;
}
memset(f, 0, sizeof f);
f[0][0] = 1;
for (int i = 1; i <= m; i++) // 多算一列算到m列,用f[m][0]代表m-1列正好铺满的方案数
for (int j = 0; j < 1 << n; j++)
for (int k = 0; k < 1 << n; k++)
if ((j & k) == 0 && st[j | k])
f[i][j] += f[i - 1][k];
cout << f[m][0] << endl;
}
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
分析

#include<iostream>
#include<iostream>
#include<cstring>
using namespace std;
const int N = 20, M = 1 << N;
int f[M][N], w[N][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];
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 给出,其中 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(u, 0):所有以u为根的子树中选择,并且不选u这个点的方案
f(u, 1):所有以u为根的子树中选择,并且选择u这个点的方案
f(u, 0):假设节点 u 有N 个子节点,则\(f ( u , 0 ) = \sum_1^n max\{ f(s_i,0), f(s_i,1) \}\),由于0表示 u 这个节点不去,则其每个子节点可以去也可以不去,因此取最大值,即取\(max \{ f(s,1), f(s,0)\}\),然后累加即可
f(u, 1):节点u去,那么他的子节点都不去,\(f(u,1)=happy_u+\) \(\sum_1^n {f(s_i,0) }\)
DFS + DP
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 6010;
int happy[N], f[N][2];
int h[N], e[N], ne[N], idx;
bool has_father[N];
int 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];
}
return;
}
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> happy[i];
memset(h, -1, sizeof h);
for (int i = 1; i < n; i++) {
int a, b;
cin >> b >> a;
add(a, b);
has_father[b] = true;
}
int root = 1;
while (has_father[root]) root++;
dfs(root);
cout << max(f[root][1], f[root][0]) << endl;
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。
接下来 RR 行,每行包含 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<cstring>
using namespace std;
const int N = 310;
int h[N][N], f[N][N];
int dx[4] = {-1, 1, 0, 0}, dy[4] = {0, 0, -1, 1};
int n, m;
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 nx = x + dx[i], ny = y + dy[i];
if (nx >= 1 && nx <= m && ny >= 1 && ny <= n && h[x][y] > h[nx][ny])
v = max(v, dp(nx, ny) + 1);
}
return v;
}
int main() {
cin >> m >> n;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
cin >> h[i][j];
memset(f, -1, sizeof f);
int res = 0;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
res = max(res, dp(i, j));
cout << res << endl;
return 0;
}

浙公网安备 33010602011771号