概念
区间DP 也属于 线性DP 中的一种,它 区间长度 作为 DP 的 阶段 ,使用两个坐标(区间的左、右端点)描述每个维度。在 区间DP 中,一个状态由若干个比它更小且包含于它的区间所代表的状态转移而来,因此 区间DP 的决策往往就是划分区间的方法。区间DP 的初态一般就由长度为1的 元区间 构成。
实现办法
区间DP 的操作步骤有其特殊性,需要按照长度递增的顺序作为阶段,先计算出长度 ≤ l e n \leq len ≤len 的所有状态,以此为基础,再计算长度为 l e n + 1 len + 1 len+1 的所有状态。直到最后计算出长度为 n n n 的状态。
一般形式:
for(int len = 2; len <= n; len++){
for(int i = 1; i <= n - len + 1; i++){
int j = i + len - 1;
dp[i][j] = inf;
for(int k = i; k < j; k++)
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + prev[j] - prev[i - 1]);
// 动态转移方程视情况而定
}
}
例题
假设只有 2堆 石子,显然只有 1种 合并方案
如果有 3堆 石子,则有 2种 合并方案,((1, 2), 3) 和 (1, (2, 3)) 如果有k堆石子呢?
不管怎么合并,总之最后总会归结为 2堆 ,如果我们把最后两堆分开,左边和右边无论怎么合并,都必须满足最优合并方案,整个问题才能得到最优解。
令m[i,j]表示归并第i个数到第j数的最小代价,w[i,j]表示第i个数到第j个数的和,这个可以事先计算出来。有如下的状态转移方程:
预处理
w
[
i
]
[
j
]
w[i][j]
w[i][j]:
先算出前缀和
s
[
i
]
s[i]
s[i],然后
w
[
i
]
[
j
]
=
s
[
j
]
–
s
[
i
−
1
]
w[i][j] = s[j] – s[i - 1]
w[i][j]=s[j]–s[i−1]
因此,
w
[
i
]
[
j
]
w[i][j]
w[i][j] 可以不事先存放,在DP时直接
O
(
1
)
O(1)
O(1) 计算
s[0] = 0;
for(int i = 1; i<=n; i++)
s[i] = s[i-1]+a[i];
核心代码:
拓展成环:
对于环状序列,通用的处理方法是把这条链延长 2倍,扩展成
2
n
−
1
2n - 1
2n−1 堆,其中 第1堆 与
n
+
1
n+1
n+1 堆完全相同,第
i
i
i 堆与
n
+
i
n+i
n+i 堆完全相同,这样我们只要对这
2
n
2n
2n 堆动态规划后,枚举
f
(
1
,
n
)
,
f
(
2
,
n
+
1
)
,
⋯
,
f
(
n
,
2
n
−
1
)
f(1, n), f(2, n + 1), \cdots ,f(n, 2n - 1)
f(1,n),f(2,n+1),⋯,f(n,2n−1) 取最优值即可即可。
2. 最长回文子串
令
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 表示在字符串区间
s
[
i
]
.
.
s
[
j
]
s[i]..s[j]
s[i]..s[j]之间的最长回文串的长度。
分几种情况讨论:
(1)如果s[i] = s[j],这时如果
s
[
i
+
1
]
⋯
s
[
j
−
1
]
s[i+1] \cdots s[j-1]
s[i+1]⋯s[j−1] 是回文串,则加上两端的回文,可构成一个更长的回文串。怎以判断
s
[
i
+
1
]
⋯
s
[
j
−
1
]
s[i+1] \cdots s[j-1]
s[i+1]⋯s[j−1] 是不是回文串呢?
一个非常简单的办法是:
d
p
[
i
+
1
]
[
j
−
1
]
dp[i+1][j-1]
dp[i+1][j−1]是否等于
j
−
i
−
1
j-i-1
j−i−1
如果
s
[
i
+
1
]
⋯
s
[
j
−
1
]
s[i+1] \cdots s[j-1]
s[i+1]⋯s[j−1] 不构成回文串,则
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 在
d
p
[
i
+
1
]
[
j
]
dp[i+1][j]
dp[i+1][j] 与
d
p
[
i
]
[
j
−
1
]
dp[i][j-1]
dp[i][j−1] 中取较大值。
(2)如果
s
[
i
]
≠
s
[
j
]
s[i] \neq s[j]
s[i]=s[j] ,则
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 在
d
p
[
i
+
1
]
[
j
]
dp[i+1][j]
dp[i+1][j] 与
d
p
[
i
]
[
j
−
1
]
dp[i][j-1]
dp[i][j−1] 中取较大值者,这仍然是一个 区间DP 的模型。
#include<bits/stdc++.h>
using namespace std;
const int Max = 3005;
char a[Max];
int dp[Max][Max], n;
int main(){
scanf("%s", a + 1);
n = strlen(a + 1);
for(int i = 1; i <= n; i++)
dp[i][i] = 1;
for(int len = 2; len <= n; len++){
for(int i = 1; i <= n; i++){
int j = i + len - 1;
if(dp[i + 1][j - 1] == len - 2 && a[i] == a[j])
dp[i][j] = len;
else
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
}
printf("%d", dp[1][n]);
return 0;
}
- 删除字符串
题目描述
给出一个长度为 n n n 的字符串,每次可以删除一个字母相同的子串,问最少需要删多少次。
定义
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 为删除区间
[
i
,
j
]
[i,j]
[i,j] 的最少次数
(1)如果
s
[
i
]
=
s
[
j
]
s[i]=s[j]
s[i]=s[j],
d
p
[
i
]
[
j
]
=
d
p
[
i
+
1
]
[
j
−
1
]
+
1
dp[i][j] = dp[i+1][j-1] + 1
dp[i][j]=dp[i+1][j−1]+1,即先删除区间
[
i
+
1
,
j
−
1
]
[i+1,j-1]
[i+1,j−1] 再把相同的
s
[
i
]
s[i]
s[i] 和
s
[
j
]
s[j]
s[j]一次删除;
(2)如果
s
[
i
]
≠
s
[
j
]
s[i] \neq s[j]
s[i]=s[j],
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
+
1
]
[
j
]
,
d
p
[
i
]
[
j
−
1
]
)
+
1
dp[i][j] = min(dp[i+1][j],dp[i][j-1]) + 1
dp[i][j]=min(dp[i+1][j],dp[i][j−1])+1,只能先删除区间
[
i
+
1
,
j
]
[i+1,j]
[i+1,j] 或者
[
i
,
j
+
1
]
[i,j+1]
[i,j+1] ,最后删除区间端点的单个字符;
但是如果有
a
a
b
b
aabb
aabb 这种串的话,上面第二种做法就要删3次,显然不对!
(3)然后枚举区间
[
i
,
j
]
[i,j]
[i,j] 的分割点
k
k
k ,
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
]
[
j
]
,
d
p
[
i
]
[
k
]
+
d
p
[
k
]
[
j
]
−
1
)
dp[i][j] = min(dp[i][j],dp[i][k]+dp[k][j]-1)
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]−1),这样的话
k
k
k 这个点删了两次,所以要
−
1
-1
−1。
#include<bits/stdc++.h>
using namespace std;
const int Max = 1005;
char s[Max];
int dp[Max][Max];
int main(){
int n;
scanf("%d",&n);
scanf("%s",&s[1]);
for(int i = 1; i <= n; i++)
dp[i][i] = 1;
for(int len = 1; len <= n; len++){
for(int i = 1; i <= n - len; i++){
int j = len + i;
if(s[i] == s[j])
dp[i][j] = dp[i + 1][j - 1] + 1;
else
dp[i][j] = min(dp[i + 1][j], dp[i][j - 1]) + 1;
for(int k = i; k <= j; k++)
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] - 1);
}
}
printf("%d",dp[1][n]);
}
分析样例:
N
=
4
N=4
N=4 , 4颗珠子的头标记与尾标记依次为(2,3) (3,5) (5,10) (10,2)。
我们用记号
⨂
\bigotimes
⨂ 表示两颗珠子的聚合操作,释放总能量:
(
(
4
⨂
1
)
⨂
2
)
⨂
3
)
=
10
×
2
×
3
+
10
×
3
×
5
+
10
×
5
×
10
=
710
((4 \bigotimes 1) \bigotimes 2) \bigotimes 3)= 10 \times 2 \times 3 + 10 \times 3 \times 5+10 \times 5 \times 10 = 710
((4⨂1)⨂2)⨂3)=10×2×3+10×3×5+10×5×10=710
该题与石子合并完全类似。
设链中的第
i
i
i 颗珠子头尾标记为
(
S
i
−
1
与
S
i
)
(S_{i} - 1与S_{i})
(Si−1与Si)
令
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 表示从第i颗珠子一直合并到第j颗珠子所能产生的最大能量,则有:
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
]
[
k
]
+
d
p
[
k
+
1
]
[
j
]
+
S
i
−
1
×
S
k
×
S
j
(
i
≤
k
<
j
)
dp[i][j] = max(dp[i][k] + dp[k+1][j] + S_{i}-1 \times S_{k} \times S_{j} (i \leq k<j)
dp[i][j]=max(dp[i][k]+dp[k+1][j]+Si−1×Sk×Sj(i≤k<j)
边界条件:
d
p
[
i
]
[
i
]
=
0
(
1
≤
i
<
k
<
j
≤
n
)
dp[i][i] = 0 (1 \leq i < k<j \leq n)
dp[i][i]=0(1≤i<k<j≤n)
项链是一个首尾闭合的环,处理方法与石子合并完全相同,即拆环为链。
时间复杂度 O ( n 3 ) O(n^{3}) O(n3)。
#include<bits/stdc++.h>
using namespace std;
const int Max = 205;
int n, a[Max];
int dp[Max][Max];
int main(){
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
a[n + i] = a[i];
}
for(int len = 2; len <= n; len++){
for(int i = 1; i <= 2 * n - len + 1; i++){
int j = i + len - 1;
for(int k = i; k < j; k++)
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j] + a[i] * a[k + 1] * a[j + 1]);
}
}
int ans = 0;
for(int i = 1; i <= n; i++)
ans = max(ans, dp[i][i + n - 1]);
printf("%d", ans);
return 0;
}