君君算法课堂-多维动态规划

多维动态规划

多维动态规划是在线性动态规划的基础上扩展状态维数,再进行状态转移的设计。

难点在于所设计的状态的维数 及 将状态设置好后,对状态的转移。

一般来说,问题越复杂,设计状态的维数就越多。

下面通过例题加深对于多维动态规划的理解。

例题:游戏

P4161 [SCOI2009]游戏

\(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

P2523 [HAOI2011]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).\)

区间动态规划

我们经常在做动态规划时遇到一类带有区间的问题。

很多时候,这类问题可以用区间动态规划解决。

在使用区间动态规划时,我们一般使用两种枚举方式。

第一种:先从小到大枚举区间长度,再枚举区间开头

第二种:先从后向前枚举区间开头,再从前向后枚举区间结尾

例题:玩具取名

P4290 [HAOI2008]玩具取名

某人有一套玩具,并想法给玩具命名。

首先他选择 \(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).\)

posted @ 2021-12-05 19:15  计网君  阅读(492)  评论(0)    收藏  举报