0x53. 动态规划 - 区间DP(习题详解 × 8)
整理的算法模板合集: ACM模板
点我看算法全家桶系列!!!
实际上是一个全新的精炼模板整合计划
文章目录
- 0x53. 动态规划 - 区间DP
- Problem A. 最优矩阵链乘
- Problem B. 括号序列
- Problem C. 矩阵取数游戏[NOIP2007 提高组]
- Problem D.262144 P(思维,优化转移)
- Problem E. 多边形
- Problem F. 金字塔
- Problem G. Two Rabbits
- Problem H. Blocks
0x53. 动态规划 - 区间DP
区间DP是线性DP的一种特殊模型,以区间长度作为DP的阶段,区间左右端点作为维度。一个状态由若干个比它更小且包含于它的区间所代表的状态转移而来。
Template 石子合并
AcWing 282
设有 NNN 堆石子排成一排,其编号为 1,2,3,…,N1,2,3,…,N1,2,3,…,N。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这 NNN 堆石子合并成为一堆。 每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。找出一种合理的方法,使总的代价最小,输出最小代价。
1≤N≤3001≤N≤3001≤N≤300
Solution
f[i,i]f[i, i]f[i,i] 表示区间合并 [l,r][l, r][l,r] 的最小代价
f[l,r]=minl≤k<r{f[l,k]+f[k+1,r]}+∑i=lraif[l, r] = \min_{l\le k <r}\{f[l, k] +f[k + 1, r]\} +\sum_{i = l}^{r}a_i f[l,r]=l≤k<rmin{f[l,k]+f[k+1,r]}+i=l∑rai
初始化:∀i∈[1,n],f[i][i]=0\forall i\in [1, n],f[i][i] = 0∀i∈[1,n],f[i][i]=0,其余为 INF。
目标:f[1,n]f[1, n]f[1,n]
直接将 l,rl, rl,r 作为 DP 的阶段转移的话, 无法保证计算 l⋯rl\cdots rl⋯r 的时候,更小的阶段 l,kl, kl,k 已经被算过了,所以我们应该以区间长度 lenlenlen 为阶段进行转移。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 300 + 7;int n, m, a[N];
int f[N][N], sum[N];int main()
{memset(f, 0x3f, sizeof f);scanf("%d", &n);for (int i = 1; i <= n; ++ i) scanf("%d", &a[i]), f[i][i] = 0, sum[i] = sum[i - 1] + a[i];for (int len = 2; len <= n; ++ len) {//阶段for (int l = 1; l + len - 1 <= n; ++ l) {//状态int r = l + len - 1;//状态for (int k = l; k < r; ++ k)//决策 f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r]);f[l][r] += sum[r] - sum[l - 1];}}cout << f[1][n] << '\n';return 0;
}
Problem A. 最优矩阵链乘
一个 n×mn\times mn×m 的矩阵由 nnn 行 mmm 列共 n×mn\times mn×m 排列而成。两个矩阵 A 和 B 可以相乘当且仅当 A 的列数等于 B 的行数。一个 n×mn\times mn×m 的矩阵乘 m×pm\times pm×p 的矩阵,运算量为 n×m×pn\times m\times pn×m×p。
矩阵乘法不满足分配律,但满足结合律。因此 A×B×CA\times B\times CA×B×C 既可以按顺序 (A×B)×C(A\times B)\times C(A×B)×C 也可以按 A×(B×C)A\times (B\times C)A×(B×C) 来进行。假设A,B,CA,B,CA,B,C 分别是 2×3,3×4,4×52\times 3,3\times 4,4\times 52×3,3×4,4×5 的,则 (A×B)×C(A\times B)\times C(A×B)×C 运算量是2×3×4+2×4×5=642\times 3\times 4+2\times 4\times 5=642×3×4+2×4×5=64,A×(B×C)A\times (B\times C)A×(B×C)的运算量是 3×4×5×2×3×5=903\times 4\times 5\times 2\times 3\times 5=903×4×5×2×3×5=90 .显然第一种顺序节省运算量。
给出 n−1n-1n−1 个矩阵组成的序列,设计一种方法把他们依次乘起来,使得总的运算量尽量小。假设第 iii 个矩阵 A[i]A[i]A[i] 是 P[i−1]×P[i]P[i-1]\times P[i]P[i−1]×P[i] 的(意味着这些矩阵可以按顺序链乘,所以只需要考虑区间相乘中区间的选取)。
POJ1651 Multiplication Puzzle
乘法游戏是在一行牌上进行的。每一张牌包括了一个正整数。在每一个移动中,玩家拿出一张牌,得分是用它的数字乘以它左边和右边的数,所以不允许拿第1张和最后1张牌。最后一次移动后,这里只剩下两张牌。你的目标是使得分的和最小。
Solution
设 f[i,j]f[i,j]f[i,j] 表示 A[i], A[i+1], ..., A[j]
乘起来的最少运算数。
转移方程:
f[i,j]=min{f[i,j],f[i,k]+f[k+1,j]+a[i]×a[k+1]×a[j+1])},i≤k<jf[i,j]=\min\{f[i,j],f[i,k] + f[k+1,j]+ a[i]\times a[k + 1] \times a[j + 1])\},i\le k<jf[i,j]=min{f[i,j],f[i,k]+f[k+1,j]+a[i]×a[k+1]×a[j+1])},i≤k<j
初始化 :f[i,i]=0f[i, i] = 0f[i,i]=0
目标:f[1][n−1]f[1][n - 1]f[1][n−1]
Code
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<iostream>
#include<cstring>using namespace std;
typedef long long ll;
const int N = 2007, M = 5000007, INF = 0x3f3f3f3f;int n, m;
int f[N][N];
int a[N];int main()
{while(scanf("%d", &n) != EOF && n){memset(f, 0x3f, sizeof f);for(int i = 1; i <= n; ++ i){scanf("%d", &a[i]);f[i][i] = 0;}for(int len = 2; len < n; ++ len){for(int i = 1; i <= n - len; ++ i){int j = i + len - 1;//注意再 -1 因为三个数才能组成两个可乘矩阵相乘f[i][j] = INF;for(int k = i; k < j; ++ k)
//!相当于是i到j但是需要的是i到j+1:例如第1个和第2个合并,对应到数上就是a[1]a[2]a[3]合并f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + a[i] * a[k + 1] * a[j + 1]);}}printf("%d\n", f[1][n - 1]);//终点是 n-1 因为三个数才能组成两个可乘矩阵相乘}return 0;
}
Problem B. 括号序列
Luogu UVA1626 Brackets sequence
我们将正规括号序列定义如下:
- 空序列是正规括号序列。
- 如果 SSS 是一个正规括号序列,那么
(S)
和[S]
都是正规括号序列。 - 如果 A 和 B 都是正规括号序列,那么 AB 是一个正规括号序列。
例如,下面这些序列都是正规括号序列: (),[],(()),([]),()[],()[()]
而下面这些不是正规括号序列: (,[,),)(,([)],([]
给你一些含有字符 '('
,')'
,'['
和 ']'
的括号序列。你需要找一个最短的正规括号序列,使给定括号序列作为一个子序列包含在其中。
Solution
显然还是区间DP。
设 f[i,j]f[{i,j}]f[i,j] 表示子串 si∼sjs_i \sim s_jsi∼sj中,使得这段子串合法还需添加的最少字符数。
对于区间左右端点 i,ji,ji,j
若 si=s_i=si=
'('
且 sj=s_j=sj=')'
,或者 si=s_i=si='['
且 sj=s_j=sj=']'
,则 f[i,j]=f[i+1,j−1]f[{i,j}]=f[{i+1,j-1}]f[i,j]=f[i+1,j−1]。
注意当 j=i+1j=i+1j=i+1 时,就会出现 f[i+1,i]f[{i+1,i}]f[i+1,i] ,所以初始化时要将 f[i+1,i]=1f[{i+1,i}]=1f[i+1,i]=1 。若 si=s_i=si=
'('
或 si=s_i=si='['
,f[i,j]=min(f[i,j],f[i+1,j]+1)f[{i,j}]=\min(f[{i,j}],f[{i+1,j}]+1)f[i,j]=min(f[i,j],f[i+1,j]+1)。若 sj=s_j=sj=
')'
或 si=s_i=si=']'
, fi,j=min(f[i,j],f[i,j−1]+1)f_{i,j}=\min(f[{i,j}],f[{i,j-1}]+1)fi,j=min(f[i,j],f[i,j−1]+1)。p(i≤p<j)p(i \leq p < j)p(i≤p<j), f[i,j]=min(f[i,j],f[i,p]+f[p+1,j])f[{i,j}]=\min(f[{i,j}],f[{i,p}]+f[{p+1,j}])f[i,j]=min(f[i,j],f[i,p]+f[p+1,j])
初始化: fi,i=1f_{i,i}=1fi,i=1
需要注意的是输入有可能是空串,所以需要用两次 getline
最后输出方案
输出时考虑四种情况:
i>j
不是子串,return 0;
i == j
子串长度为1
说明是一个孤立点,补齐输出,return 1;
s = (s')
或s = [s']
那么返回的是f(i + 1, j - 1)
- 其他情况,枚举断点
Code
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<iostream>
#include<cstring>using namespace std;
typedef long long ll;
const int N = 507, M = 5000007, INF = 0x3f3f3f3f;
const double eps = 1e-6;int n, m, t;
int a[N];
int f[N][N];
int b[N];
string s;bool match(char a, char b)
{if(a == '[' && b == ']')return true;if(a == '(' && b == ')')return true;return false;
}//按照转移的方法完全的再倒推一遍
void print(int i, int j)
{if(i > j)return ;if(i == j){if(s[i] == '(' || s[i] == ')')printf("()");else printf("[]");return ;}int ans = f[i][j];if(match(s[i], s[j]) && ans == f[i + 1][j - 1]){printf("%c", s[i]);print(i + 1, j - 1);//往里缩printf("%c", s[j]);//包起来return ;}for(int k = i; k < j; ++ k){if(ans == f[i][k] + f[k + 1][j]){print(i, k);print(k + 1, j);return ;}}
}int main()
{scanf("%d", &t);getchar();while(t -- ){memset(f, 0x3f, sizeof f);getline(cin, s);//注意因为可能有空串所以要输入两次getline(cin, s);n = s.length();if(n == 0){puts("");puts("");continue;}//f[i][j]表示s[i~j]至少要添加多少个括号for(int i = 1; i < n; ++ i){f[i][i] = 1;f[i][i - 1] = 0;}f[0][0] = 0;for(int i = n - 1; i >= 0; -- i){//左for(int j = i + 1; j < n; ++ j){//右//f[i][j] = INF;if(match(s[i], s[j]))//方案1,相同可内缩f[i][j] = min(f[i][j], f[i + 1][j - 1]);for(int k = i; k < j; ++ k){//方案2,至少大于2,可以分开f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j]);}}}print(0, n - 1);puts("");if(t)puts("");}return 0;
}
Problem C. 矩阵取数游戏[NOIP2007 提高组]
Luogu P1005
帅帅经常跟同学玩一个矩阵取数游戏:对于一个给定的 n×mn \times mn×m 的矩阵,矩阵中的每个元素 ai,ja_{i,j}ai,j 均为非负整数。游戏规则如下:
- 每次取数时须从每行各取走一个元素,共 nnn 个。经过 mmm 次后取完矩阵内所有元素;
- 每次取走的各个元素只能是该元素所在行的行首或行尾;
- 每次取数都有一个得分值,为每行取数的得分之和,每行取数的得分 = 被取走的元素值 ×2i\times 2^i×2i ,其中 iii 表示第 iii 次取数(从 111 开始编号);
- 游戏结束总得分为 mmm 次取数得分之和。
帅帅想请你帮忙写一个程序,对于任意矩阵,可以求出取数后的最大得分。
Solution
首先显然每一行之间是没有任何关联的,所以我们可以每一行分开做,nnn 行的得分求和即使答案。
对于每一行来说,我们设 f[i,j]f[i, j]f[i,j] 表示区间 [i,j][i, j][i,j] 的最大得分。
由于我们每次只能从这一行的两端取数,所以我们只需要考虑两种情况即可。我们按照正常的区间DP转移即可。题目中第 iii 次取数,要乘上 2i2^i2i,我们每次乘以 222 即可,即第 iii 次取数,是由第 i−1i-1i−1 次取数的 2i−1×2=2i2^{i-1}\times 2 = 2^i2i−1×2=2i 转移过来。
- 取区间左端的数 a[l]a[l]a[l],显然有 f[l,r]=max{2×f[l+1,r]+2×a[l]}f[l, r] = \max\{2 \times f[l +1, r] + 2 \times a[l]\}f[l,r]=max{2×f[l+1,r]+2×a[l]}
- 取区间右端的数 a[r]a[r]a[r],显然有 f[l,r]=max{2×f[l,r−1]+2×a[r]}f[l, r] = \max\{2 \times f[l, r - 1] + 2 \times a[r]\}f[l,r]=max{2×f[l,r−1]+2×a[r]}
由于这里我们并不是合并操作,而是取数,即一个数的时候也会取走,并产生贡献,所以我们循环的时候,区间长度 len∈[0,m]len\in[0, m]len∈[0,m],并且因此 r=l+lenr= l + lenr=l+len ,因为 lenlenlen 可以等于 000。
最后显然答案至少可以达到 2802^{80}280,所以需要用 __int128
或者高精计算。
Code
#include <bits/stdc++.h>
using namespace std;
#define int __int128
const int N = 107;template<typename T> inline void read(T& t)
{int f = 0, c = getchar();t = 0;while(!isdigit(c))f |= c == '-', c = getchar();while(isdigit(c)) t = t * 10 + c - 48, c = getchar();if(f)t = -t;
}template<typename T> inline void print(T x)
{if(x < 0) x = -x, putchar('-');if(x > 9) print(x / 10);putchar(x % 10 + 48);
}
//f[i, j] 表示区间 [i, j] 的最大得分int n, m;
int f[N][N];
int a[N];signed main()
{read(n), read(m);int ans = 0;while (n -- ) {memset(f, 0, sizeof f);for (int i = 1; i <= m; ++ i)read(a[i]);for (int len = 0; len <= m; ++ len) {for (int l = 1; l + len <= m; ++ l) {int r = l + len;f[l][r] = max(2 * f[l + 1][r] + 2 * a[l], 2 * f[l][r - 1] + 2 * a[r]);}}ans += f[1][m];}print(ans);puts("");return 0;
}
Problem D.262144 P(思维,优化转移)
Luogu P3147 [USACO16OPEN]
Bessie 被她最近玩的一款游戏迷住了,游戏一开始有 nnn 个正整数,(2≤n≤262144)(2\le n\le 262144)(2≤n≤262144),范围在 1∼401\sim 401∼40。在一步中,贝西可以选相邻的两个相同的数,然后合并成一个比原来的大一的数(例如两个 777 合并成一个 888 ),目标是使得最大的数最大,请帮助 Bessie 来求最大值。
Solution
首先显然是区间 DP ,但是这里合并的条件是两个相邻的数相同,所以普通的 f[i,j]f[i, j]f[i,j] 表示区间 [i,j][i,j][i,j] 的阶段显然不能求解答案。
所以我们显然是要将当前区间内的数 xxx 当做一个阶段。区间的左右端点我们显然是要知道一个的,然后区间的长度也是要清楚的。由于我们需要得到的是区间内能合成的最大的数是谁,所以可以保存是一个 bool
类的判断,即设 f[i,j,k]f[i, j, k]f[i,j,k] 表示区间左端点在 iii,区间长度为 jjj,能否合并出数 kkk。
根据题意,n≤262144n\le 262144n≤262144,显然能合成的最大的涨幅不超过 18,218>26214418,2^{18}>26214418,218>262144,给定的数 1≤ai≤401\le a_i\le 401≤ai≤40,所以最多我们能得到的数不超过 40+18=5840 + 18 = 5840+18=58,所以我们可以枚举区间能合成的数 kkk 来作为转移的阶段。
但是这里三重循环,需要枚举左端点,区间长度,和合并的数,空间和时间都是 O(n2logn)O(n^2\log n)O(n2logn) 显然要炸,我们希望能有二重循环, O(nlogn)O(n\log n)O(nlogn) 的复杂度才能通过此题。
考虑优化,如何消去一个维度。显然先前设置的 bool
类的 fff 数组保存一维信息。我们原先设置 bool
类数组是希望可以判断能否合成数 kkk,如果将原先 bool
类的信息替换掉,我们替换的信息必须具备可以判断是否能合成这个数,且可以由两个区间合并时求出来。显然我们可以选择区间长度这一信息,令 fff 保存区间长度, 若 f≠0f\neq 0f=0,说明我们可以从两个区间转移到这个区间,也就说明可以合成这个数,这样若 f=0f=0f=0 ,说明并没有转移到这个数,也就不能合成这个数。
综上所述,我们可以设 f[i,j]f[i, j]f[i,j] 表示区间左端点 iii,合成的数为 jjj 的区间长度。
显然两个 j−1j-1j−1 的区间合并之后,区间长度为两个区间的长度之和,即:
f[i][j]=f[i][j−1]+f[i+f[i][j−1]][j−1]f[i][j]=f[i][j - 1]+f[i+f[i][j - 1]][j - 1]f[i][j]=f[i][j−1]+f[i+f[i][j−1]][j−1]
f[i,j]f[i, j]f[i,j] 可由两个相邻的区间并为 [i,i+f[i,j−1]][i, i+f[i,j - 1]][i,i+f[i,j−1]] 的区间合并而来,左区间左端点为 iii,右端点为 i+f[i,j−1]i+f[i,j -1]i+f[i,j−1],右区间左端点为 i+f[i,j−1]i+f[i,j - 1]i+f[i,j−1],右端点为 i+f[i,j−1]+f[i+f[i,j−1]][j−1]i+f[i, j - 1]+f[i+f[i, j - 1]][j - 1]i+f[i,j−1]+f[i+f[i,j−1]][j−1]。 显然我们在转移的时候要先判断一下这两个左右区间是否存在,即 fff 是否不为 000,否则不存在的区间合成的不存在的数就没有意义了。
我们只需要枚举能合成的数,以及左端点的位置,即可进行转移,时间复杂度为 O(nlogn)O(n\log n)O(nlogn)
初始化:f[i][ai]=1f[i][a_i] = 1f[i][ai]=1
转移的时候判断 f[i,j]f[i, j]f[i,j] 是否不为 000 取最大的 jjj 即为答案。
Code
#include <bits/stdc++.h>
using namespace std;
const int maxn = 3e5 + 7, maxai = 58;int n, ans;
int a[maxn];
int f[maxn][maxai];int main()
{scanf("%d", &n);for (int i = 1; i <= n; ++ i) scanf("%d", &a[i]), f[i][a[i]] = 1;for (int j = 1; j <= maxai; ++ j) {for (int i = 1; i <= n; ++ i) {if(f[i][j] == 0) if(f[i][j - 1] && f[i + f[i][j - 1]][j - 1])f[i][j] = f[i][j - 1] + f[i + f[i][j - 1]][j - 1];if(f[i][j])ans = j;}}cout << ans << '\n';return 0;
}
Problem E. 多边形
AcWing 283
多边形游戏”是一款单人益智游戏。
游戏开始时,给定玩家一个具有 NNN 个顶点 NNN 条边(编号 1∼N1\sim N1∼N)的多边形。
每个顶点上写有一个整数,每个边上标有一个运算符 +++(加号)或运算符 ∗*∗(乘号)。
第一步,玩家选择一条边,将它删除。
接下来在进行 N−1N−1N−1 步,在每一步中,玩家选择一条边,把这条边以及该边连接的两个顶点用一个新的顶点代替,新顶点上的整数值等于删去的两个顶点上的数按照删去的边上标有的符号进行计算得到的结果。
最终,游戏仅剩一个顶点,顶点上的数值就是玩家的得分。
请计算对于给定的 NNN 边形,玩家最高能获得多少分,以及第一步有哪些策略可以使玩家获得最高得分。
Solution
显然我们如果直接枚举删除哪一条边,然后问题就变成了石子合并。但是没必要暴力枚举,这里是一个环,我们显然可以破环成链,即先选择任意删除一条边,我们令被删除的边逆时针方向的顶点称为第一个顶点,按连接顺序编号,然后把剩下得到的链复制到链的后面,得到长度为 2n2n2n 的链,即可破环成链,我们就可以直接在链上循环,从 O(n2)O(n^2)O(n2) 降到了 O(2n)O(2n)O(2n)。
然后考虑该 “石子合并” 问题。很直观的可以设 f[l,r]f[l, r]f[l,r] 为第 lll 到第 rrr 个顶点合并为一个顶点后最大顶点上的最大值。
但是题目中拥有 +,×+,\times+,× 两种操作,且给定的数据中可能有负数,因此区间 [l,r][l, r][l,r] 的最大值并不一定是由两个最大值自区间[l,k],[k+1,r][l, k], [k + 1, r][l,k],[k+1,r] 合并而来,即不满足最优子结构。
仅维护最大值,可能会被两个最小值(两个负数)相乘得到更大的数给顶替掉,却不能维护到这种情况。
为了解决这个问题,显然我们可以一块维护一下最小值,即同时维护最大值和最小值这两种信息。
因此我们可以
设 f[l,r,0]f[l, r, 0]f[l,r,0] 表示为第 lll 到第 rrr 个顶点合并为一个顶点后顶点上的最大值。
设 f[l,r,1]f[l, r, 1]f[l,r,1] 表示为第 lll 到第 rrr 个顶点合并为一个顶点后顶点上的最小值
我们只需要枚举区间的划分点 kkk (决策),得到转移方程:
f[l,r,0]=maxl≤k<r{max{f[l,k,0]op f[k+1,r,0]op ∈{+,∗}f[l,k,p]∗f[k+1,r,q]p,q∈{0,1}}f[l,r,1]=minl≤k<r{min{f[l,k,1]op f[k+1,r,1]op ∈{+,∗}f[l,k,p]∗f[k+1,r,q]p,q∈{0,1}}\begin{array}{l}\displaystyle f[l, r, 0]=\max _{l \leq k<r}\left\{\max \left\{\begin{array}{ll}f[l, k, 0] \text { op } f[k+1, r, 0] & \text { op } \in\{+, *\} \\f[l, k, p] * f[k+1, r, q] & p, q \in\{0,1\}\end{array}\right\}\right. \\\\\displaystyle f[l, r, 1]=\min_{l \leq k<r} \left\{\min \left\{\begin{array}{c}f[l, k, 1] \text { op }f[k+1, r, 1] &\text { op } \in\{+, *\} \\f[l, k, p] * f[k+1, r, q] &p, q \in\{0,1\}\end{array}\right\}\right.\end{array} f[l,r,0]=l≤k<rmax{max{f[l,k,0] op f[k+1,r,0]f[l,k,p]∗f[k+1,r,q] op ∈{+,∗}p,q∈{0,1}}f[l,r,1]=l≤k<rmin{min{f[l,k,1] op f[k+1,r,1]f[l,k,p]∗f[k+1,r,q] op ∈{+,∗}p,q∈{0,1}}
初始化:∀i∈[1,n],f[l,l,0]=f[l,l,1]=ai\forall i\in [1, n], f[l, l, 0] = f[l, l, 1] = a_i∀i∈[1,n],f[l,l,0]=f[l,l,1]=ai,其余 f[l,r,0]f[l, r, 0]f[l,r,0] 为负无穷,f[l,r,1]f[l, r, 1]f[l,r,1] 为正无穷。
目标:f[1,n,0]f[1, n, 0]f[1,n,0]
复杂度为 O(n3)O(n^3)O(n3)。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 200 + 7, INF = 0x3f3f3f3f;int n, m;
char c[N];
int a[N];
int f[N][N][2];int main() { scanf("%d", &n);for (int i = 1; i <= n; ++ i) cin >> c[i] >> a[i]; for (int i = n + 1; i <= (n << 1); ++ i) c[i] = c[i - n], a[i] = a[i - n]; for (int i = 1; i <= (n << 1); ++ i)for (int j = 1; j <= (n << 1); ++ j)f[i][j][0] = -INF, f[i][j][1] = INF;for (int i = 1; i <= (n << 1); ++ i) f[i][i][0] = f[i][i][1] = a[i];for (int len = 2; len <= n; ++ len) {for (int l = 1; l + len - 1 <= (n << 1); ++l) {int r = l + len - 1;for (int k = l + 1; k <= r; ++k)if (c[k] == 't') {f[l][r][0] = max(f[l][r][0], f[l][k - 1][0] + f[k][r][0]);f[l][r][1] = min(f[l][r][1], f[l][k - 1][1] + f[k][r][1]);} else {f[l][r][0] = max(f[l][r][0], max(f[l][k - 1][0] * f[k][r][0], f[l][k - 1][1] * f[k][r][1]));f[l][r][1] = min(f[l][r][1], min(f[l][k - 1][0] * f[k][r][0], min(f[l][k - 1][1] * f[k][r][1], min(f[l][k - 1][0] * f[k][r][1], f[l][k - 1][1] * f[k][r][0]))));}}}int ans = -INF;for (int i = 1; i <= n; ++ i) ans = max(ans, f[i][i + n - 1][0]);cout << ans << endl;for (int i = 1; i <= n; ++ i)if (ans == f[i][i + n - 1][0])printf("%d%c", i, i == n ? '\n' : ' ');return 0;
}
Problem F. 金字塔
AcWing 284
金字塔由若干房间组成,房间之间连有通道。
如果把房间看作节点,通道看作边的话,整个金字塔呈现一个有根树结构,节点的子树之间有序,金字塔有唯一的一个入口通向树根。
并且,每个房间的墙壁都涂有若干种颜色的一种。
探险队员打算进一步了解金字塔的结构,为此,他们使用了一种特殊设计的机器人。
这种机器人会从入口进入金字塔,之后对金字塔进行深度优先遍历。
机器人每进入一个房间(无论是第一次进入还是返回),都会记录这个房间的颜色。
最后,机器人会从入口退出金字塔。
显然,机器人会访问每个房间至少一次,并且穿越每条通道恰好两次(两个方向各一次), 然后,机器人会得到一个颜色序列 SSS。
但是,探险队员发现这个颜色序列并不能唯一确定金字塔的结构。
现在他们想请你帮助他们计算,对于一个给定的颜色序列,有多少种可能的结构会得到这个序列。
因为结果可能会非常大,你只需要输出答案对 10910^9109 取模之后的值。
S.size≤300S.size\le 300S.size≤300
Solution
这里给定的颜色序列 SSS 与树的 DFS序 很像,只不过这里使用的是可以重复的颜色来组成序列,但是这里的序列仍然具备子树在序列里是一个连续区间的性质,因此我们就可以使用序列的子区间表示子树组成一整棵树即整个颜色序列。
我们可以想到一种状态标识 f[l,r]f[l,r]f[l,r] 代表区间 [l,r][l,r][l,r] 所代表的树能组成的 树 的方案总数。
根据给定的类 DFS 序列的性质,区间 [l,r][l, r][l,r] 中,s[l+1],s[r−1]s[l+ 1],s[r - 1]s[l+1],s[r−1] 这两个字符都代表着从子树的根开始进入子树和离开子树时记录的颜色序列,字符 s[l]=s[r]s[l]=s[r]s[l]=s[r] 代表 区间 [l,r][l,r][l,r] 可能是同一颗子树,并且显然至少三个点的颜色序列才能组成一颗子树。
那么如何划分区间进行DP转移呢?如果直接暴力将子串 s[l∼r]s[l\sim r]s[l∼r] 直接分成两部分,每部分有若干棵子树构成,可能会产生重复计数,因为划分的子串可能前后相等,重复计数。为了保证计数不重不漏,可以只考虑字串 S[l∼r]S[l\sim r]S[l∼r] 的第一棵子树是由哪一段构成的。
枚举划分点 kkk ,令字串 S[l+1∼k−1]S[l+1\sim k-1]S[l+1∼k−1] 构成 [l,r][l,r][l,r] 的第一棵子树,S[k+1∼r]S[k +1\sim r]S[k+1∼r] 构成 [l,r][l,r][l,r] 的剩余部分,即其他子树。如果 kkk 不相同,那么字串 S[l+1k−1]S[l+1~k-1]S[l+1 k−1] 代表的子树大小也不相同,故不可能产生重复计算。
于是就有了一种状态转移方程:
f[l,r]={0Sl≠Srf[l+1,r−1]+∑l+2≤k≤r−2,Sl=Skf[l+1,k−1]×f[k,r]Sl=Sr\displaystyle f[l, r]=\left\{\begin{array}{ll}0 && S_{l} \neq S_{r} \\\displaystyle f[l+1, r-1]+ &\displaystyle \sum_{l+2 \leq k \leq r-2, S_{l}=S_{k}} f[l+1, k-1]\times f[k, r] & S_{l}=S_{r}\end{array}\right. f[l,r]=⎩⎨⎧0f[l+1,r−1]+l+2≤k≤r−2,Sl=Sk∑f[l+1,k−1]×f[k,r]Sl=SrSl=Sr
初始化: f[l][l]=1,l∈[1,N]f[l][l] = 1, l∈[1,N]f[l][l]=1,l∈[1,N], 其余均为 000。
目标:f[1,n]f[1, n]f[1,n]
我们需要判断 s[l]==s[r]s[l]==s[r]s[l]==s[r] ,因为区间左右端点相等才说明他们是同一颗子树
对于记录方案数这种DP问题,通常一个状态的各个决策之间满足 “加法原理” ,而每个决策的划分的几个子状态之间满足 “乘法原理” 。而一个状态的所有决策之间必须 互斥,才能保证不会出现重复问题。
Code
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 300 + 7, mod = 1e9;char s[N];
ll n, m;
ll f[N][N];ll solve(int l, int r)
{if(l > r) return 0;if(l == r) return 1;if(f[l][r] != -1) return f[l][r]; f[l][r] = 0;for (int k = l + 2; k <= r; ++ k) //枚举第一棵子树在s中划分的结束位置if(s[l] == s[k])f[l][r] = (f[l][r] + solve(l + 1, k - 1) * solve(k, r)) % mod;return f[l][r];
}int main()
{scanf("%s", s + 1); memset(f, -1, sizeof f);n = strlen(s + 1);cout << solve(1, n) << '\n';return 0;
}
Problem G. Two Rabbits
HDU 4745
两只兔子,在 nnn 块石头围成一个环上跳跃,第 iii 块石头有权值 aia_iai,一只顺时针跳,一只逆时针跳,每跳一次,两只兔子所在的石头的权值都要相等,问在一圈内(各自不能超过各自的起点,也不能再次回到起点)它们最多能经过多少个石头。
Solution
要求每一步都必须相等,且可以选择性地跳向某些石头,所以显然答案就是环内的最长回文子序列。
所以我们直接破环成链,将环倍增至链。
设 f[l,r]f[l, r]f[l,r] 表示 区间 [l,r][l,r][l,r] 内最长的回文子序列长度。
考虑区间两端 l,rl,rl,r
若 a[l] == a[r]
,显然有 f[l,r]=max{f[l+1,r−1]+2}f[l,r] = \max\{f[l + 1,r - 1]+2\}f[l,r]=max{f[l+1,r−1]+2}
若 a[l] != a[r]
,显然有 f[l,r]=max{f[l+1,r],f[l,r−1]}f[l,r] = \max\{f[l + 1,r],f[l, r - 1]\}f[l,r]=max{f[l+1,r],f[l,r−1]}
初始化:f[i,i]=1,i∈[1,n]f[i,i] = 1,i\in[1,n]f[i,i]=1,i∈[1,n]
考虑最后的答案位置,由于题目中两只兔子最开始站立的位置没有限制:
若两只兔子站立在同一个石头上,如石头 111,则它们一只可以从 111 跳到 nnn ,一只可以从 111 跳到 222 ,计算区间长度的时候,需要去掉相同的起点 111 ,即区间长度 len=n−1\mathrm{len} = n-1len=n−1。因为这个起点是重叠的,如果直接让 len=n\mathrm {len=n}len=n ,含义是区间 [1,n][1,n][1,n] 和区间 [n,1][n,1][n,1],显然起点会被多算一次。最后再加上起点算作经过的同一个石头,答案为 f[i,i+n−1−1]+1f[i, i + n - 1-1]+1f[i,i+n−1−1]+1( len=n−1,r=l+len−1\mathrm{len}=n-1,r=l+\mathrm{len}-1len=n−1,r=l+len−1 )。
若两只兔子站立在两块石头上,如石头 111 和 nnn,则它们一只可以从 111 跳到 nnn ,它们一只可以从 nnn 跳到 111 ,跳跃长度为 nnn,DP答案选择区间长度 nnn,答案为 f[i,i+n−1]f[i,i + n - 1]f[i,i+n−1](同理)。
取最大值即可。
Code
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2000 + 7, maxm = 2000 + 7;
int n, m, s, t, k, ans;
int a[maxn << 1];
int f[maxn][maxn];int main()
{while(scanf("%d", &n) != EOF && n) {memset(f, 0, sizeof f);for (int i = 1; i <= n; ++ i) scanf("%d", &a[i]), a[i + n] = a[i];for (int i = 1; i <= 2 * n; ++ i)f[i][i] = 1;for (int len = 2; len <= n; ++ len) {for (int l = 1; l + len - 1 <= 2 * n; ++ l) {int r = l + len - 1;f[l][r] = max(f[l + 1][r], f[l][r - 1]);if(a[l] == a[r])f[l][r] = max(f[l][r], f[l + 1][r - 1] + 2);}}int ans = 0;for (int i = 1; i <= n; ++ i)ans = max({ans, f[i][i + n - 1],f[i][i + n - 2] + 1});cout << ans << '\n';}return 0;
}
Problem H. Blocks
POJ 1390
给你一个颜色块序列,每次你可以删除一些相同颜色并且相邻的颜色块,并获得删除数目平方的收益,现在给你一个颜色块序列,问收益最大是多少?
Solution
Code
#include <bits/stdc++.h>
using namespace std;
const int maxn = 200 + 7;
int n, m, s, t, ans, kcase;
int f[maxn][maxn][maxn];
int a[maxn];int dp(int i, int j, int k) {if(i > j) return 0;int& ans = f[i][j][k];if(i == j) return ans = (1 + k) * (1 + k);if(ans != -1) return ans;int p = j;while (p >= i && a[p] == a[j]) -- p;++ p;ans = dp(i, p - 1, 0) + (k + j - p + 1) * (k + j - p + 1);for (int q = i; q < p; ++ q) {if(a[q] == a[j] && a[q + 1] != a[q]) {ans = max(ans, dp(i, q, k + j - p + 1) + dp(q + 1, p - 1, 0));}}return ans;
}int main() { scanf("%d", &t);while(t -- ) {printf("Case %d: ", ++ kcase);scanf("%d", &n);for (int i = 1; i <= n; ++ i)scanf("%d", &a[i]);memset(f, -1, sizeof f);printf("%d\n", dp(1, n, 0));}
}
0x53. 动态规划 - 区间DP(习题详解 × 8)相关推荐
- 0x52. 动态规划 - 背包(习题详解 × 19)
目录 0x52. 动态规划 - 背包 0x52.1 0/10/10/1 背包 Problem A. 数字组合 Problem B. 背包问题求具体方案 Problem C. jury Compromi ...
- 【算法之动态规划(一)】动态规划(DP)详解
一.基本概念 动态规划(dynamic programming)是 运筹学 的一个分支,是求解决策过程(decision process)最优化的数学方法.20世纪50年代初 美国 数学家R.E.Be ...
- 状态压缩动态规划部分习题详解
状态压缩动态规划部分习题详解 状压DP部分题目详解 状态压缩动态规划部分习题详解 简介 经典子集类问题 原子弹 最短路与状压DP结合 送礼物 P3959宝藏 旅游 经典网格类 铺地砖 一笔画 其他类型 ...
- 数据结构(C语言版) 第 六 章 图 知识梳理 + 习题详解
目录 一. 图的基本定义和术语 一.图的基本概念 1.度 2.连通 (1)连通图 (2)强连通/强连通图 3.回路 4.完全图 二.图的三种存储结构 1.邻接矩阵表示法 2.邻接表(链式)表示法 3. ...
- 超级棒的一个DP问题详解(入门)
超级棒的一个DP问题详解(入门) 只要耐心看肯定可以理解的~动态规划问题故事描述~ 通过金矿模型介绍动态规划 附上原文地址: http://www.cnblogs.com/sdjl/articles/ ...
- c语言将AOE网络的数据写入TXT文档中,数据结构与算法学习辅导及习题详解.张乃孝版-C/C++文档类资源...
数据结构与算法学习辅导及习题详解.张乃孝版.04年10月 经过几年的努力,我深深体会到,编写这种辅导书要比编写一本湝通教材困难得多. 但愿我的上述理想,在本书中能够得以体现. 本书的组织 本书继承了& ...
- 数据结构(C语言版) 第 八 章 排序 知识梳理 + 习题详解
目录 一.归并排序 二.交换排序 1.快速排序 2.冒泡排序 三.插入排序 1.直接插入排序(基于顺序查找) 2.折半插入排序(基于折半查找) 3.希尔排序(基于逐趟缩小增量) 四.选择排序 0.直接 ...
- Python面对对象编程——结合面试谈谈封装、继承、多态,相关习题详解
1.面向对象的三大特征 封装:属性和方法放到类内部,通过对象访问属性或者方法,隐藏功能的实现细节.当然还可以设置访问权限; 继承:子类需要复用父类里面的属性或者方法,当然子类还可以提供自己的属性和方法 ...
- 数学物理方法pdf_《数学物理方法》周明儒(第2版)补充材料与习题详解
说明: 1.本资源为江苏师范大学 周明儒 教授所编著<数学物理方法>(第二版)配套电子资源: 2.由于Abook限制下载原版PDF,故本人高清截图保存整理为PDF格式供使用该书的同学使用, ...
最新文章
- 触摸矫正+android,android触摸矫正解方程
- WCF服务重构实录(上)
- 《OpenGL编程指南》一第2章 着色器基础
- cad去水印_CAD神技巧神插件,助你绘图效率提高N倍!
- 从贝泰妮的全域消费者运营,看Quick Audience如何链接产品服务商生态
- javascript底层练习
- swift php 类型判断,iOS开发 - Swift实现检测网络连接状态及网络类型
- 配置View桌面时找不到域的解决方法
- C++(STL):25 ---序列式容器stack源码剖析
- 【数据结构】数据结构知识思维导图
- Linux——粘滞位(sbit)、sgid、suid 权限
- 咕咕(数位dp+AC自动机)
- 7.1 pdo 宝塔面板php_腾讯云服务器建站系列 – 熟练宝塔面板部署网站/快速安装HTTPS加密...
- scrt 上传和下载文件
- 微信公众号小程序与服务号和订阅号有什么区别
- java 计算正态分布_使用Java计算正态分布
- (构造笔记)GRASP学习心得
- 增加客流量的方法_7种成熟的方法来增加网站流量
- 测序是测量你的遗传信息
- 实例分割模型Mask R-CNN详解:从R-CNN,Fast R-CNN,Faster R-CNN再到Mask R-CNN
热门文章
- 轻松学Pytorch – 行人检测Mask-RCNN模型训练与使用
- Opencv实战 | 用摄像头自动化跟踪特定颜色物体
- 第六篇:协调和协定之组通信
- 撩课-Java面试宝典-第十二篇
- vue - config(index.js)
- JavaEE框架整合之基于注解的SSH整合
- String.fromCharCode()
- Eclipse打JAR包,插件FatJar安装与使用
- python非贪婪、多行匹配正则表达式例子[转载]
- php提交字段打印,在隱藏的表單字段中提交的JSON編碼的PHP數組 - 在提交表單的頁面中打印時會產生奇怪的輸出...