区间合并类动态规划也属于线性动规。它以区间的长度为阶段,使用区间的两个端点来描述这个状态(如 F [ i ] [ j ] F[i][j] F[i][j])。在区间DP中,一个阶段通常由若干个比它更小且包含于它的区间所代表的的状态转移过来。
所谓区间问题,就是给出的问题是进行按区间的性质进行,要么合并,要么分解为区间操作。
下面我们来看看区间类型的动态规划解题思路。
例1:石子合并NOI1995
题面:石子合并
这是一道十分经典的区间合并问题。看上去是将两堆石子合并,实际上就是将两个相邻的区间
f
[
i
]
[
j
]
f[i][j]
f[i][j] 和
f
[
j
+
1
]
[
k
]
f[j+1][k]
f[j+1][k] 合并,得到一个新的区间
f
[
i
]
[
k
]
f[i][k]
f[i][k]。那么状态转移方程就是:
f
[
i
]
[
j
]
=
max
{
f
[
i
]
[
k
]
+
f
[
k
+
1
]
[
j
]
+
w
[
i
]
+
w
[
i
+
1
]
+
⋯
+
w
[
j
]
}
(
i
≤
k
<
j
)
f[i][j]=\max\{f[i][k]+f[k+1][j]+w[i]+w[i+1]+\cdots +w[j] \} (i \le k < j)
f[i][j]=max{f[i][k]+f[k+1][j]+w[i]+w[i+1]+⋯+w[j]}(i≤k<j)
同时,
w
[
i
]
+
w
[
i
+
1
]
+
⋯
+
w
[
j
]
w[i]+w[i+1]+\cdots +w[j]
w[i]+w[i+1]+⋯+w[j] 的值可以用前缀和进行替代。那么状态转移方程就是:
f
[
i
]
[
j
]
=
max
{
f
[
i
]
[
k
]
+
f
[
k
+
1
]
[
j
]
+
s
u
m
[
j
]
−
s
u
m
[
i
−
1
]
}
(
i
≤
k
<
j
)
f[i][j]=\max\{f[i][k]+f[k+1][j]+sum[j]-sum[i-1] \} (i \le k < j)
f[i][j]=max{f[i][k]+f[k+1][j]+sum[j]−sum[i−1]}(i≤k<j)
例2:Polygon问题IOI1998
题面:Polygon
第一眼看上去,这题与上面那题好像差不多,就只要多枚举一遍断掉的边就可以了。
但是,不要忘记动态规划有一个十分重要的前提:满足最有子结构。如果按照上一题的做法,那么区间 [ l , r ] [l,r] [l,r] 的最大值应该是由两个区间的最大值相加或相乘得到。但是这很容易就可以举出反例:
区间 [ i , k ] [i,k] [i,k] 的最大值为 5 5 5,最小值为 − 7 -7 −7;
区间 [ k + 1 , j ] [k+1,j] [k+1,j] 的最大值为 6 6 6,最小值为 − 8 -8 −8;
如果区间 [ i , j ] [i,j] [i,j] 是通过这两个区间相乘得到,那么最大值应该是两个最小的数相乘 ( − 7 ) × ( − 8 ) = 56 (-7) \times (-8) = 56 (−7)×(−8)=56,而不是两个最大的数相乘!
显然不能像上面一样做(不然只能拿80分),但这同时也提供一种新的思路:我们可以将区间最大最小值同时作为一个区间的代表信息,也就是下面这个表格
区间最大值 可以由 | 区间最小值 可以由 |
---|---|
两个最大值相加 | 两个最小值相加 |
两个最大值相乘 | 两个最小值相乘 |
两个最小值相乘 | 两个最大值相乘 |
最大值与最小值相乘 | 最大值和最小值相乘 |
得到 | 得道 |
可以用
F
[
l
,
r
,
0
]
F[l,r,0]
F[l,r,0] 表示区间
[
l
,
r
]
[l,r]
[l,r] 的最大值,用
F
[
l
,
r
,
1
]
F[l,r,1]
F[l,r,1] 表示
[
l
,
r
]
[l,r]
[l,r] 的最小值。那么状态转移方程如下:
{
F
[
l
,
r
,
0
]
=
max
{
F
[
l
,
k
,
0
]
o
p
t
F
[
k
+
1
,
r
,
0
]
(
o
p
t
∈
{
+
,
×
}
)
F
[
l
,
k
,
p
]
×
F
[
k
+
1
]
[
r
]
[
q
]
(
p
,
q
∈
{
0
,
1
}
)
F
[
l
,
r
,
1
]
=
min
{
F
[
l
,
k
,
1
]
o
p
t
F
[
k
+
1
,
r
,
1
]
(
o
p
t
∈
{
+
,
×
}
)
F
[
l
,
k
,
p
]
×
F
[
k
+
1
,
r
,
q
]
(
p
,
q
∈
{
0
,
1
}
)
\begin{cases} F[l,r,0] = \max \begin{cases} F[l,k,0] \ opt \ F[k+1,r,0] \ (opt \in \{+,\times \}) \\ \\ F[l,k,p] \times F[k+1][r][q] \ (p,q \in \{0,1 \})\\ \end{cases} \\ \\ F[l,r,1] = \min \begin{cases} F[l,k,1] \ opt \ F[k+1,r,1] \ (opt \in \{+,\times \})\\ \\ F[l,k,p] \times F[k+1,r,q] \ (p,q \in \{0,1 \}) \\ \end{cases} \end{cases}
⎩⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎧F[l,r,0]=max⎩⎪⎨⎪⎧F[l,k,0] opt F[k+1,r,0] (opt∈{+,×})F[l,k,p]×F[k+1][r][q] (p,q∈{0,1})F[l,r,1]=min⎩⎪⎨⎪⎧F[l,k,1] opt F[k+1,r,1] (opt∈{+,×})F[l,k,p]×F[k+1,r,q] (p,q∈{0,1})
这样的话这个算法的时间复杂度为
O
(
n
4
)
O(n^4)
O(n4)。其实我们可以断环成链,这样就可以省去一个循环。具体实现请看下面这份代码:
#include <cstdio>
#include <cstring>
#include <algorithm>
#define M 200
#define Inf 0x3f3f3f3f
using namespace std;
int n, m, ans=0;
int f1[M][M], f2[M][M], a[M];
char c[M],space1,space2;
inline int read()
{
int re=0, f=1; char ch=getchar();
while(ch<'0' || ch>'9') {if(ch=='-') f=-1; ch=getchar();}
while(ch>='0' && ch<='9') {re=re*10+(ch-'0'); ch=getchar();}
return re*f;
}
int main()
{
memset(f1,0x3f,sizeof(f1));
memset(f2,-0x3f,sizeof(f2));
n=read();
for(int i=1;i<=n;++i)
{
getchar();
scanf("%c%d",&c[i],&a[i]);
a[i+n]=a[i], c[i+n]=c[i];
}
// for (int i=1;i<=2*n;++i) printf("%c%d",c[i],a[i]);
// printf("\n");
for(int i=1;i<=2*n;++i) f1[i][i]=f2[i][i]=a[i];
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+1;k<=j;++k)
{
if(c[k]=='t')
{
f1[i][j]=min(f1[i][j],f1[i][k-1]+f1[k][j]);
f2[i][j]=max(f2[i][j],f2[i][k-1]+f2[k][j]);
}
else
{
f2[i][j]=max(f2[i][j],f2[i][k-1]*f2[k][j]);
f2[i][j]=max(f2[i][j],f1[i][k-1]*f1[k][j]);
f1[i][j]=min(f1[i][j],f1[i][k-1]*f1[k][j]);
f1[i][j]=min(f1[i][j],f2[i][k-1]*f1[k][j]);
f1[i][j]=min(f1[i][j],f1[i][k-1]*f2[k][j]);
f1[i][j]=min(f1[i][j],f2[i][k-1]*f2[k][j]);
}
}
}
}
ans=-Inf;
for(int i=1;i<=n;++i) ans=max(ans,f2[i][i+n-1]);
printf("%d\n",ans);
// printf("%d\n",f2[1][n]);
for(int i=1;i<=n;++i) if(f2[i][i+n-1]==ans) printf("%d ",i);
return 0;
}
例3:Convex CountourCF838E
英文题面:Convex Countour
中文题面:Convex Countour 中文
一句话题意:按顺时针给出一个凸包(凸多边形),任意两点之间有一条直的边,求经过每个点恰好一次的最长不自交路径。
首先观察题目的性质。
由于是凸包,因此不自交路径中的一条边 ( x , y ) (x,y) (x,y) 的两端点只能向与 x x x 或 y y y 相邻的结点连边。
举个栗子,若选取了一条边 ( x , y ) (x,y) (x,y),且假设编号从 x x x 到 y y y 结点已经在一条不自交路径中(不考虑特殊情况),那么向外扩展路径只能连向相邻的点,即只能连边 ( x + 1 , y ) (x+1, y) (x+1,y) 或 ( x , x + 1 ) (x, x+1) (x,x+1) 或 ( x , y − 1 ) (x, y-1) (x,y−1) 或 ( y − 1 , y ) (y-1, y) (y−1,y)。
很容易用反证法证明。假设连边 ( x − 2 , y ) (x-2, y) (x−2,y),那么点 x − 1 x-1 x−1 无法通过一条不与 ( x , y ) (x, y) (x,y) 或 ( x − 2 , y ) (x-2, y) (x−2,y) 相交的路径与其他点连通。而此题路径要覆盖所有点,即所有点之间连通,则矛盾。因此上述结论成立。
由于选取的路径每次只能向外扩展一个点,那么此题就变成了区间动态规划问题。
设 F [ l , r , 0 / 1 ] F[l, r, 0/1] F[l,r,0/1] 表示区间 [ l , r ] [l, r] [l,r] 的最长路径长度, 0 0 0 表示路径终点在 l l l, 1 1 1 表示路径终点在 r r r。
那么状态转移方程就是
{
F
[
l
,
r
,
0
]
=
max
{
F
[
l
+
1
,
r
,
0
]
+
d
i
s
[
l
,
l
+
1
]
F
[
l
+
1
,
r
,
1
]
+
d
i
s
[
l
,
r
]
F
[
l
,
r
,
1
]
=
max
{
F
[
l
,
r
−
1
,
0
]
+
d
i
s
[
r
,
l
]
F
[
l
,
r
−
1
,
1
]
+
d
i
s
[
r
,
r
−
1
]
\begin{cases} F[l,r,0] = \max \begin{cases} F[l+1,r,0]+dis[l,l+1] \\ \\ F[l+1,r,1]+dis[l,r] \\ \end{cases} \\ \\ F[l,r,1] = \max \begin{cases} F[l,r-1,0]+dis[r,l] \\ \\ F[l,r-1,1]+dis[r,r-1] \\ \end{cases} \end{cases}
⎩⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎧F[l,r,0]=max⎩⎪⎨⎪⎧F[l+1,r,0]+dis[l,l+1]F[l+1,r,1]+dis[l,r]F[l,r,1]=max⎩⎪⎨⎪⎧F[l,r−1,0]+dis[r,l]F[l,r−1,1]+dis[r,r−1]
那么代码就在下面
#include <bits/stdc++.h>
#define N 2510
using namespace std;
struct Point {
double x, y;
}p[N];
double f[N][N][2];
int n;
double dis(Point a, Point b)
{
return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
int main()
{
scanf("%d",&n);
for(int i=0;i<=n-1;i++)
{
int x, y;
scanf("%d%d",&x,&y);
p[i].x=x/1.0, p[i].y=y/1.0;
}
for(int len=2;len<=n;len++)
{
for(int l=0;l<n;l++)
{
int r=(l+len-1)%n;
f[l][r][0]=max(f[(l+1)%n][r][0]+dis(p[l],p[(l+1)%n]), f[(l+1)%n][r][1]+dis(p[l],p[r]));
f[l][r][1]=max(f[l][(r-1+n)%n][0]+dis(p[r], p[l]), f[l][(r-1+n)%n][1]+dis(p[r],p[(r-1+n)%n]));
}
}
double ans=0;
for(int i=0;i<=n-1;i++) ans=max(ans,max(f[i][i+n-1][0],f[i][(i+n-1)%n][1]));
printf("%.10lf\n",ans);
return 0;
}