1、acwing 282 石子合并 区间dp基础题
设有 N 堆石子排成一排,其编号为 1,2,3,…,N。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
例如有 4 堆石子分别为 1 3 5 2
, 我们可以先合并 1、2堆,代价为 1+3=4,得到 4 5 2
,
第二步是先合并 2,3 堆,则代价为 5+2=7,得到 4 7
,
最后一次合并代价为 4+7=11,总代价为 4+7+11=22。
问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
思考方式:闫氏DP分析法
核心思想:考虑最后一次合并的状态转换:必定是由左边连续的一部分 + 右边连续的一部分
1、 d p [ i ] [ j ] dp[i][j] dp[i][j]: 表示将区间 i 到 j 的石子合并成一堆的最小值
集合:区间i到j的石子的合并方案,属性:min
2、状态转移方程:
d p [ i ] [ j ] = d p [ i ] [ k ] + d p [ k + 1 ] [ j ] + ∑ i j a [ i ] dp[i][j] = dp[i][k] + dp[k+1][j] + \sum_{i}^{j}a[i] dp[i][j]=dp[i][k]+dp[k+1][j]+∑ija[i],
其中连续的区间和: s u m [ j ] − s u m [ i − 1 ] sum[j] - sum[i-1] sum[j]−sum[i−1], 前缀和预处理
3、区间DP的初始化:
- 所有 d p [ i ] [ j ] dp[i][j] dp[i][j] = INF, i != j
- d p [ i ] [ i ] = 0 dp[i][i] = 0 dp[i][i]=0, 区间为1的合并消耗的都为0
4、枚举方式:复杂度 O(n^3)
- 枚举len的区间长度:一般从2到n
- 枚举左端点 i 从1开始, 右端点:j = i + len - 1 , 并且保证 j <= n
- 枚举 k 值 ,在 i 到 j 之间
- 枚举左端点 i 从1开始, 右端点:j = i + len - 1 , 并且保证 j <= n
AC代码:
#include <bits/stdc++.h>
using namespace std;
int n,a[310],sum[310],dp[310][310];
//dp[i][j]:合并从i到j的石子的区间最值
int main(){
cin>>n;
for (int i = 1; i <= n; i ++ ) cin>>a[i], sum[i] = sum[i-1] + a[i];
memset(dp, 0x3f, sizeof(dp));
for (int i = 1; i <= n; i ++ ) dp[i][i] = 0; //区间为 len = i - i + 1
for(int len = 2; len<=n ;len++){
for (int i = 1; i+len-1 <= n; i ++ ){
int j = i + len - 1;
for(int k = i; k<=j; k++){
dp[i][j] = min(dp[i][j] , dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1]);
}
}
}
cout<<dp[1][n];
}
2、AcWing 1068. 环形石子合并 环形DP问题
将 n 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。
规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。
请编写一个程序,读入堆数 n 及每堆的石子数,并进行如下计算:
- 选择一种合并石子的方案,使得做 n−1 次合并得分总和最小。
- 选择一种合并石子的方案,使得做 n−1 次合并得分总和最大。
输入样例:
4
4 5 9 4
输出样例:
43
54
思考方式:尽可能转化为链式的思路(加长一段)
加长一段后:所有可能的环形合并石子的方式都映射在新的链式问题上了
最后只需要遍历一遍,即可得到最后的值,复杂度: O(n^3)
AC代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 410;
int n,f[N][N], g[N][N], a[N], sum[N]; //f:min , g:max
int main()
{
cin>>n;
for (int i = 1; i <= n; i ++ ) cin>>a[i], sum[i] = sum[i-1] + a[i];
for(int i=n+1; i<=2*n; i++) sum[i] = sum[i-1] + a[i-n];
memset(f,0x3f,sizeof(f));
memset(g,-0x3f,sizeof(g));
for (int i = 1; i <= 2*n; i ++ ) f[i][i] = g[i][i] = 0;
for(int len = 2; len<=n; len++){
for (int i = 1; i+len-1 <= 2*n; i ++ ){
int j = i + len - 1;
for(int k=i;k<=j;k++){
f[i][j] = min(f[i][j] , f[i][k] + f[k+1][j] + sum[j] - sum[i-1]);
g[i][j] = max(g[i][j] , g[i][k] + g[k+1][j] + sum[j] - sum[i-1]);
}
}
}
int minn = 0x3f3f3f, maxn = -0x3f3f3f;
for (int i = 1; i <= n; i ++ ){
minn = min(minn , f[i][i+n-1]);
maxn = max(maxn , g[i][i+n-1]);
}
cout<<minn<<endl;
cout<<maxn<<endl;
}
3、Acwing 320 能量项链 环形DP
在 Mars 星球上,每个 Mars 人都随身佩带着一串能量项链,在项链上有 N 颗能量珠。
能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。
并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。
因为只有这样,通过吸盘(吸盘是 Mars 人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。
如果前一颗能量珠的头标记为 m,尾标记为 r,后一颗能量珠的头标记为 r,尾标记为 n,则聚合后释放的能量为 m×r×n(Mars 单位),新产生的珠子的头标记为 m,尾标记为 n。
需要时,Mars 人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。
显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。
例如:设 N=4,4 颗珠子的头标记与尾标记依次为 (2,3)(3,5)(5,10)(10,2)。
我们用记号 ⊕ 表示两颗珠子的聚合操作,(j⊕k)表示第 j,k 两颗珠子聚合后所释放的能量。则
第 4、1两颗珠子聚合后释放的能量为:(4⊕1)=10×2×3=60。
这一串项链可以得到最优值的一个聚合顺序所释放的总能量为 ((4⊕1)⊕2)⊕3)=10×2×3+10×3×5+10×5×10=710。
解法:同第二题
状态转移方程稍微修改一下:
f[i][j] : 区间i到j石子最大能产生的能量
f[i][j] = max(f[i][k] + f[k+1][j] + a[i].l * a[k].r * a[j].r , f[i][j]);
AC代码:
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 210;
struct node{
ll l,r;
}a[N];
ll n;
ll f[N][N]; //区间i到j石子最大能产生的能量
// f[i][j] = max(f[i][k] + f[k+1][j] + a[i].l * a[k].r * a[j].r , f[i][j]);
int main(){
cin>>n;
for (int i = 1; i <= n; i ++ ) cin>>a[i].l;
for (int i = 1; i <= n; i ++ ) a[i].r = a[i+1].l;
a[n].r = a[1].l;
for (int i = n+1; i <= n*2; i ++ ) a[i].l = a[i-n].l , a[i].r = a[i-n].r;
//初始化
memset(f, -0x3f3f3f3f , sizeof(f));
for (int i = 1; i <= n*2; i ++ ) f[i][i] = 0;
for(int len = 2; len <= n; len++){
for(int i=1; i + len - 1 <= 2*n; i++){
int j = i + len - 1;
for(int k = i; k<=j ;k++){
f[i][j] = max(f[i][k] + f[k+1][j] + a[i].l * a[k].r * a[j].r , f[i][j]);
}
}
}
ll maxn = -0x3f3f3f3f;
for (int i = 1; i <= n; i ++ ) maxn = max(maxn, f[i][i+n-1]);
cout<<maxn<<endl;
}
4、acwing 479 加法二叉树 中序遍历区间DP
设一个 n 个节点的二叉树 tree 的中序遍历为(1,2,3,…,n),其中数字 1,2,3,…,n 为节点编号。
每个节点都有一个分数(均为正整数),记第 i 个节点的分数为 di,tree 及它的每个子树都有一个加分,任一棵子树 subtree(也包含 tree 本身)的加分计算方法如下:
subtree的左子树的加分 × subtree的右子树的加分 + subtree的根的分数
若某个子树为空,规定其加分为 1。
叶子的加分就是叶节点本身的分数,不考虑它的空子树。
试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树 tree。
要求输出:
(1)tree的最高加分
(2)tree的前序遍历
思路:求一个树根的分数 = max(左子树*右子树+自己分数)
尝试用区间dp来解:前提是已知中序遍历:左-根-右
d p [ i ] [ j ] dp[i][j] dp[i][j]:从第i个结点到第j个结点的最大得分
状态转移方程:k为root时
d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i ] [ k − 1 ] ∗ d p [ k + 1 ] [ j ] + w [ k ] ) dp[i][j] = max( \ dp[i][j] \ , \ dp[i][k-1] * dp[k+1][j] + w[k] \ ) dp[i][j]=max( dp[i][j] , dp[i][k−1]∗dp[k+1][j]+w[k] )
重点注意下:初始化
d p [ i ] [ i ] = w [ i ] , r o o t [ i ] [ i ] = i dp[i][i] = w[i] \ ,\ root[i][i] = i dp[i][i]=w[i] , root[i][i]=i
AC代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 40;
int dp[N][N] , root[N][N];
int w[N],n;
vector<int> v;
void dfs(int l, int r){
if(l > r) return;
int k = root[l][r];
v.push_back(k);
if(k!=l) dfs(l, k-1);
if(k!=r) dfs(k+1, r);
}
int main(){
cin>>n;
for (int i = 1; i <= n; i ++ ) cin>>w[i];
for (int i = 1; i <= n; i ++ ) dp[i][i] = w[i], root[i][i] = i;
for(int len = 2; len <= n; len ++){
for (int i = 1; i+len-1 <= n; i ++ ){
int j = i + len - 1;
for(int k=i; k<=j ;k++){
int left = (k==i) ? 1 : dp[i][k-1];
int right = (k==j) ? 1 : dp[k+1][j];
int x = left * right + w[k];
if(dp[i][j] < x ){
dp[i][j] = x;
root[i][j] = k;
}
}
}
}
dfs(1,n);
cout<<dp[1][n]<<endl;
for (int i = 0; i < v.size(); i ++ ) cout<<v[i]<<" ";
}
补题:2021蓝桥杯国赛C:最小权值
拖了很久了,很可惜,当时一直考虑完全二叉树各类性质,应该用DP的思想去解决!
题面:对于一棵有根二叉树 T,小蓝定义这棵树中结点的权值 W(T) 如下:
空子树的权值为 0
如果一个结点 v 有左子树 L, 右子树 R,分别有 C(L) 和 C(R) 个结点,则
- W ( v ) = 1 + 2 W ( L ) + 3 W ( R ) + ( C ( L ) ) 2 C ( R ) W(v) = 1 + 2W(L) + 3W(R) + (C(L))^2 C(R) W(v)=1+2W(L)+3W(R)+(C(L))2C(R)
树的权值定义为树的根结点的权值。
小蓝想知道,对于一棵有 2021 个结点的二叉树,树的权值最小可能是多少?
#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll dp[5000];
//dp[i]:结点个数为i的树,根节点的最小权值
//dp[i] = min(dp[i] , 1 + 2*dp[j] + 3*dp[i-j] + j*j*(i-j));
int main(){
memset(dp, 0x3f3f3f3f, sizeof(dp));
dp[0] = 0;
for(int i=1;i<=2021;i++){
for(int j=0;j<i;j++){
dp[i] = min(dp[i] , 1 + 2*dp[j] + 3*dp[i-j-1] + j*j*(i-j-1));
}
}
cout<<dp[2021]<<endl;
}
运行结果:2653631372