君君算法课堂-多维动态规划
多维动态规划
多维动态规划是在线性动态规划的基础上扩展状态维数,再进行状态转移的设计。
难点在于所设计的状态的维数 及 将状态设置好后,对状态的转移。
一般来说,问题越复杂,设计状态的维数就越多。
下面通过例题加深对于多维动态规划的理解。
例题:游戏
\(windy\) 学会了一种游戏。
对于 \(1\) 到 \(n\) 这 \(n\) 个数字,都有唯一且不同的 \(1\) 到 \(N\) 的数字与之对应。
最开始 \(windy\) 把数字按顺序 \(1,2,3,…,n\) 写一排在纸上。
然后 再在这一排下面写上它们对应的数字。
然后又在新的一排下面写上它们对应的数字。
如此反复,直到序列再次变为 \(1,2,3,…,n\)。
问对于所有可能的对应关系,有多少种可能的排数。
数据范围 \(n ≤ 1000\)
题解:这个对应关系相当于一个置换。
而排列数则是这个置换中所有环大小的最小公倍数。
问题转化为求和为 \(n\) 的多个数有多少种可能的最小公倍数。
显然所有数都表示为 \(p^k\) 的形式下是最优的。
用 \(f[i][j]\) 表示考虑到第 \(i\) 个质数,前面总和为 \(j\) 的最小公倍数方案数。
\(f[i][j] = \displaystyle\sum_k (f[i−1][j−p_i^k])\)
时间复杂度 \(O(n^2)\),空间复杂度 \(O(n^2).\)
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long LL;
const int N = 1e3 + 5;
int read() {
int x = 0, f = 1; char ch = getchar();
while(! isdigit(ch)) f = (ch=='-')?-1:1, ch = getchar();
while(isdigit(ch)) x = (x<<3)+(x<<1)+(ch^48), ch = getchar();
return x * f;
}
int n, pri[N], cnt;
LL f[N], ans;
bool vis[N];
void Prime() {
vis[0] = vis[1] = 1;
for(int i = 2; i <= N; i ++) {
if(vis[i] == 0) pri[++ cnt] = i;
for(int j = 1; j <= cnt && i * pri[j] <= N; j ++) {
vis[i * pri[j]] = 1;
if(i % pri[j] == 0) break;
}
}
}
int main() {
Prime();
if((n = read()) == 1) { printf("1\n"); return 0; }
f[0] = 1;
for(int pos = 1; pos <= cnt; pos ++) {
for(int j = n; j >= pri[pos]; j --) {
int tmp = pri[pos];
while(tmp <= j) {
f[j] += f[j - tmp];
tmp *= pri[pos];
}
}
}
for(int i = 1; i <= n; i ++) ans += f[i];
printf("%lld\n", ans + 1);
return 0;
}
例题:Problem c
给 \(n\) 个人安排座位,先给每个人一个 \(1\) ∼ \(n\) 的编号,
设第 \(i\) 个人的编号为 \(a_i\)(不同人的编号可以相同),
接着从第一个人开始,大家依次入座,第 \(i\) 个人来了以后尝试坐到 \(a_i\),
如果 \(a_i\) 被占据了,就尝试 \(a_{i+}\),\(a_{i+1}\) 也被占据了的话就尝试 \(a_{i+2}, . . . ,\)
如果一直尝试到第 \(n\) 个都不行,该安排方案就不合法。
有 \(m\) 个人的编号已经钦定,你只能安排剩下的人的编号,
求有多少种合法的安排方案。
由于答案可能很大, 只需输出其除以 \(M\) 后的余数即可。
数据范围 \(n ≤ 300\)
题解:发现如果有不到 \(i\) 个人满足 \(a_j ≤ i\),则不合法。
状态 \(f[i][j]\) 表示有 \(j\) 个初始不确定的人满足 \(a_k ≤ i\) 的方案数。
用 \(b\) 数组表示有 \(b_i\) 个人 \(a_j\) 必须小于等于 \(i\) 。
那么得到 \(f[i][j] = \sum(f[i - 1][j−k * C_j^k])(j + b_i ≥ i)\)
其中 \(C_j^k\) 可以使用杨辉三角形预处理。
时间复杂度 \(O(n^3)\),空间复杂度 \(O(n^2).\)
例题:排队
有 \(n\) 个魔法师,被编号为 \(1\) ∼ \(n\),现在要将他们按一定顺序排队。
排完队之后,每个魔法师希望,自己的相邻的两人只要无一个人的编号和自己的编号相差为 \(1\)(\(+1\) 或 \(-1\))就行;
现在想知道,存在多少方案满足这个条件, 对 \(1000000007\) 取模。
数据范围 n ≤ 1000
题解:由于从前向后一个一个加入情况太过复杂,所以我们可 以考虑从小到大一个个加入。
状态 \(f[i][j][k]\) 表示 \(1\) ∼ \(i\) ,其中有 \(j\) 对不合法的,\(i\) 和 \(i-1\) 是否连在一起,\(k=0\) 表示靠在一起,\(k=1\) 表示没有靠在一起。
\(f[i][j][0] = f[i−1][j+1][0] * (j + 1) + f[i−1][j][0] * (i − j − 1) + f[i−1][j+1][1] * j + f[i−1][j][1] * (i − j)\)
\(f[i][j][1] = f[i−1][j][0] * 2 + f[i−1][j][1] + f[i−1][j−1][1]\)
时间复杂度 \(O(n^2)\),空间复杂度 \(O(n^2).\)
例题:最长等差数列
给定 \(n\) 个不同的正整数,问选出其中一些数组成最长的等差数列有多长。
数据范围 \(n ≤ 5000,a_i ≤ 10^9\)
题解:首先将所有数从小到大排序。
容易想到用 \(f[i][j]\) 表示以 \(i\) 为结尾,\(j\) 为倒数第二个数的最长等差数列。
\(f[i][j]=f[j][k] + 1(a[i] + a[k] = a[j] * 2)\)
由于在先枚举 \(i\),再枚举 \(j\) 的情况下确定 \(k\) 比较困难。
所以我们可以考虑先枚举\(j\),这样我们在从前向后枚举i的时候,
也可以将 \(k\) 从后向前推,快速求出对应的值。
时间复杂度 \(O(n^2)\),空间复杂度 \(O(n^2).\)
区间动态规划
我们经常在做动态规划时遇到一类带有区间的问题。
很多时候,这类问题可以用区间动态规划解决。
在使用区间动态规划时,我们一般使用两种枚举方式。
第一种:先从小到大枚举区间长度,再枚举区间开头
第二种:先从后向前枚举区间开头,再从前向后枚举区间结尾
例题:玩具取名
某人有一套玩具,并想法给玩具命名。
首先他选择 \(WING\) 四个字母中的任意一个字母作为玩具的基本名字。
然后他会根据自己的喜好,将名字中任意一个字母用 “\(WING\)” 中任意两个字母代替,
使得自己的名字能够扩充得很长。
\(m\) 种代替方案都会以 \(A− > BC\) 的形式给出。
现在,他想请你猜猜一个长度为 \(n\) 的名字,最初可能是由哪几个字母变形过来的。
数据范围 \(n ≤ 200, m ≤ 64\)
题解:状态 \(f[l][r][x]\) 表示区间 \([l,r]\) 能否变成 \(x\) 这个字母。
转移时只要枚举所有变换方案,之后枚举区间的分割点即可。
\(f[l][r]][a_i] = f[l][k][b_i] or f[k+1][r][c_i]\)
时间复杂度 \(O(m * n^3)\),空间复杂度 \(O(4 * n^2).\)
由于这样复杂度较高,所以可以使用位运算优化计算过程。
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
char S[10], s[10], SS[210], A[100], B[100], C[100];
int f[210][210][30], n, cnt, N[5];
int main() {
strcpy(S, "WING");
for(int i = 0; i < 4; i ++) scanf("%d", &N[i]);
for(int j = 0; j < 4; j ++) {
for(int i = 1; i <= N[j]; i ++) {
scanf("%s", s);
A[++ cnt] = S[j];
B[cnt] = s[0];
C[cnt] = s[1];
}
}
scanf("%s", SS + 1);
n = strlen(SS + 1);
for(int i = 1; i <= n; i ++) f[i][i][SS[i] - 'A'] = 1;
for(int i = n; i >= 1; i --)
for(int j = i + 1; j <= n; j ++)
for(int k = i; k < j; k ++)
for(int l = 1; l<= cnt; l ++)
f[i][j][A[l] - 'A'] |= f[i][k][B[l] - 'A'] & f[k + 1][j][C[l] - 'A'];
int flag = 0;
for(int i = 0; i < 4; i ++)
if(f[1][n][S[i] - 'A']) putchar(S[i]), flag = 1;
if(flag)puts(""); else puts("The name is wrong!");
return 0;
}
例题:涂色
给定一条长度为 \(n\) 的木板,初始时,木板没有颜色。
一次操作可以将一段连续的区间染成一种颜色,后染的颜色会覆盖之前染的颜色。
现在希望你将木板第 \(i\) 格染成 \(a_i\) 的颜色。 问最少操作数。
数据范围 \(n ≤ 200\)
题解:容易想到用状态 \(f[x][y]\) 表示区间 \([x,y]\) 染成目标颜色最少的步数。
那么最基础的转移 \(f[x][y] = max_x\leq mid<y(f[x][mid]+f[mid + 1][y])\)
如果 \(a_x = a_{x+1}\),那么有 \(f[x][y] = max(f[x][y] , f[x+1][y] )\)
如果 \(a_y = a_{y-1}\),那么有 \(f[x][y] = max(f[x][y] , f[x][y-1] )\)
如果 \(a_x = a_y\),那么有 \(f[x][y] = max(f[x][y], f[x+1][y] , f[x][y-1])\)
时间复杂度 \(O(n^3)\),空间复杂度 \(O(n^2).\)
例题:合唱队
有 \(n\) 个人,第 \(i\) 个人的身高是 \(H_i\),现在要按照顺序将这些人插入合唱队列中。
如果第 \(i\) 个人比第 \(i-1\) 个人高,那将他插入合唱队最右边, 否则插入合唱队的最左边。
现在给出合唱队的最终情况,问有多少种初始情况可以构造出给出的最终情况,
答案对 \(1000000007\)取模。
数据范围 \(n ≤ 1000\)
题解:容易想到每一时刻队形中的人都是最终队形的一个子区间。
\(f[l][r]\) 表示最终队形 \([l,r]\) 中的所有人,最后加入的是第 \(l\) 个人的方案数。
\(g[l][r]\) 表示最终队形 \([l,r]\) 中的所有人,最后加入的是第 \(r\) 个人的方案数。
\(f[l][r] = f[l+1][r] * [H_{l+1} > H_l ] + g[l+1][r] * [H_r > H_l ]\)
\(g[l][r] = f[l][r−1] * [H_l < H_r ] + g[l][r−1] * [H_{r−1} < H_r ]\)
时间复杂度 \(O(n^2)\),空间复杂度 \(O(n^2).\)
例题:区间问题
有一个长度为 \(n\) 的数列,其上有 \(m\) 个区间 \((l_i ,r_i)\),每个区间有一定的价值 \(a_i\),保证不会有两个重合的区间。
现在希望你选择一些区间,满足任意两个区间是包含关系或者相离关系,并最大化价值和。
数据范围 \(n ≤ 300, m ≤ \frac{n(n+1)}{2}\)
题解:首先我们简化问题。
考虑区间之间只能相离不能包含怎么做。
用状态 \(f_i\) 表示最后一个区间以 \(i\) 为结尾的答案。
得到 \(f[r_i] = max(f[0], . . . , f[l_i−1]) + a_i\)
记录 \(f\) 数组的前缀最大值,并将所有区间挂在其 \(r_i\) 点上即 可。
时间复杂度 \(O(n^2)\),空间复杂度 \(O(n^2).\)
考虑如何处理包含问题。
用 \(g[l][r]\) 来表示区间 \([l,r]\) 中的最大值。
那么对于每一个区间,我们都跑一遍之前不带包含的 \(dp\) 即可。
注意,其中的区间价值要用 \(g[l_i][r_i]\) 代替。 时间复杂度 \(O(n^4)\),空间复杂度 \(O(n^2)\)。
发现对于所有 \(l\) 相同的区间,不带包含的 \(dp\) 可以只跑一遍。
所以我们从后向前枚举区间开头,再从前向后枚举区间结尾即可。
时间复杂度 \(O(n^3)\),空间复杂度 \(O(n^2).\)

浙公网安备 33010602011771号