Luckyleaves's Blog

stay hungry,stay foolish greedy and lucky

区间dp

讲课,ps

什么是区间dp

区间 dp 就是在区间上进行动态规划,求解一段区间上的最优解。主要是通过合并小区间的最优解进而得出整个大区间上最优解的 dp 算法(实际上不需要是最优解)。

石子合并问题

有若干个石子排成一行,每个石子有一定的质量,现在要将它们合并成一堆,每一次合并产生的价值是两堆石子的质量和(只能合并相邻的石子),求最大/最小价值。

以最小值为例,对于这个问题,如果我们用fi,j表示从ij的代价最小值,那么这一块区域可以被分割成两个小的区域,通过不断调整分割点,我们就可以找到fi,j,转移方程如下:
fi,j=min(fi,j,fi,k+fk+1,j+s[j]s[i1])(ik<j)
初始值:
fi,i=0
时间复杂度O(n3)

如果石子排成了一个环呢?

环形石子合并问题

一般来说,有下面几种转换方法:

  1. 把环变成链,枚举那个缺口,时间复杂度O(n4)
  2. 考虑到变成环之后,问题转换为求n条链上的石子合并问题,那么可以把长度为n的链拆开,复制一次,变成长度为2n的链,用石子合并的方法求出fi,j后,求出min(fi,i+n1),时间复杂度O(8n3)

显然第二种更好。

洛谷-P1880-[NOI1995] 石子合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>

const int N = 205, PosInf = 1e9;

int n;
int a[N], s[N], f[N][N], g[N][N];

int main()
{
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
{
if (i == j)
f[i][i] = g[i][i] = 0;
else
{
f[i][j] = PosInf;
g[i][j] = -PosInf;
}
}

std::cin >> n;
for (int i = 1; i <= n; i++)
{
std::cin >> a[i];
a[i + n] = a[i];
}
for (int i = 1; i <= n * 2; i++)
s[i] = s[i - 1] + a[i];

for (int len = 1; len <= n; len++)
for (int l = 1; l + len - 1 <= n * 2; l++)
{
int r = l + len - 1;
for (int k = l; k < r; k++)
{
f[l][r] = std::min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
g[l][r] = std::max(g[l][r], g[l][k] + g[k + 1][r] + s[r] - s[l - 1]);
}
}

int wMin = PosInf, wMax = -PosInf;
for (int l = 1; l <= n; l++)
{
wMin = std::min(wMin, f[l][l + n - 1]);
wMax = std::max(wMax, g[l][l + n - 1]);
}
std::cout << wMin << std::endl;
std::cout << wMax << std::endl;
return 0;
}

问题拓展

AcWing-320-能量项链

本题和前面的合并略有不同,待合并的两个区间不是由点构成,而是由点与点之间的空隙构成。因此区间长度至少是3,至多是n+1,且断点k不能和左端点相等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>

const int N = 205, PosInf = 1e9;

int n;
int a[N], f[N][N];

int main()
{
std::cin >> n;
for (int i = 1; i <= n; i++)
{
std::cin >> a[i];
a[i + n] = a[i];
}

for (int len = 3; len <= n + 1; len++)
for (int l = 1; l + len - 1 <= n * 2; l++)
{
int r = l + len - 1;
for (int k = l + 1; k < r; k++)
f[l][r] = std::max(f[l][r], f[l][k] + f[k][r] + a[l] * a[k] * a[r]);
}

int wMax = -PosInf;
for (int l = 1; l <= n; l++)
wMax = std::max(wMax, f[l][l + n]);
std::cout << wMax << std::endl;
return 0;
}

LibreOJ-10149-凸多边形的划分

可以将一个多边形的划分拆为三部分:左,中(三角形),右。最小价值即为这三者的价值和,于是可以发现转移方程和能量项链类似:
fi,j=min(fi,j,fi,k+fk,j+wiwkwj)
注意,本题数据较大,需要做高精度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <iostream>
#include <cstring>

const int N = 55, M = 35, PosInf = 1e9;

int n, w[N];
long long f[N][N][M];

void Add(long long a[], long long b[])
{
static long long c[M];
memset(c, 0, sizeof(c));
for (int i = 0, t = 0; i < M; i++)
{
t += a[i] + b[i];
c[i] = t % 10;
t /= 10;
}
memcpy(a, c, sizeof(c));
}

void Mul(long long a[], long long b)
{
static long long c[M];
memset(c, 0, sizeof(c));
long long t = 0;
for (int i = 0; i < M; i++)
{
t += a[i] * b;
c[i] = t % 10;
t /= 10;
}
memcpy(a, c, sizeof(c));
}

int Comp(long long a[], long long b[])
{
for (int i = M - 1; i >= 0; i--)
if (a[i] > b[i])
return 1;
else if (a[i] < b[i])
return -1;
return 0;
}

int main()
{
std::cin >> n;
for (int i = 1; i <= n; i++)
std::cin >> w[i];

long long temp[M];
for (int len = 3; len <= n + 1; len++)
for (int l = 1; l + len - 1 <= n; l++)
{
int r = l + len - 1;
f[l][r][M - 1] = 1;
for (int k = l + 1; k < r; k++)
{
memset(temp, 0, sizeof(temp));
temp[0] = w[l];
Mul(temp, w[k]);
Mul(temp, w[r]);
Add(temp, f[l][k]);
Add(temp, f[k][r]);
if (Comp(f[l][r], temp) > 0)
memcpy(f[l][r], temp, sizeof(temp));
}
}

int k = M - 1;
while (k > 0 && f[1][n][k] == 0)
k--;
for (int i = k; i >= 0; i--)
std::cout << f[1][n][i];
std::cout << std::endl;
return 0;
}

AcWing-479-加分二叉树

这个题乍一看似乎和动态规划没有关系,但我们仔细分析“中序”,可以发现,正如区间中用点分割,中序遍历中,左右子树和根正好可以通过区间型动态规划的经典处理方式划分。因此用fi,j表示中序遍历为输入数组的ij位的二叉树中加分的最大值,但由于要输出具体方案,还要用gi,j表示中序遍历为输入数组的ij位的二叉树的根的序号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>

const int N = 35;

int n, w[N];
int f[N][N], g[N][N];

void Print(int l, int r)
{
if (l > r)
return;

int root = g[l][r];
std::cout << root << " ";
Print(l, root - 1);
Print(root + 1, r);
}

int main()
{
std::cin >> n;
for (int i = 1; i <= n; i++)
std::cin >> w[i];

for (int len = 1; len <= n; len++)
for (int l = 1; l + len - 1 <= n; l++)
{
int r = l + len - 1;
if (len == 1)
{
f[l][r] = w[l];
g[l][r] = l;
}
else
{
for (int k = l; k <= r; k++)
{
int subl = (k == l) ? 1 : f[l][k - 1];
int subr = (k == r) ? 1 : f[k + 1][r];
int score = subl * subr + w[k];
if (f[l][r] < score)
{
f[l][r] = score;
g[l][r] = k;
}
}
}
}

std::cout << f[1][n] << std::endl;
Print(1, n);
return 0;
}

DCOJ2008. 蜜雪冰城

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include<bits/stdc++.h>
using namespace std;
const int N = 55, M = 4005;
int n, m;
int a[M], b[M], c[M];
int f[N][N][M], g[N][N][M];
vector<int> s;
int cnt[M];
void init(int l, int r, int k)
{
memset(cnt, 0, sizeof(cnt));
for(int i = 1;i <= m;i ++)
if(l <= a[i] && b[i] <= r && a[i] <= k && k <= b[i]) cnt[c[i]] ++;
}
int ba[N][N][M], reba[N][N][M];
void out(int l, int r, int id)
{
int wh = ba[l][r][id];
if(l <= wh - 1) out(l, ba[l][r][id] - 1, reba[l][wh - 1][id]);
printf("%d ", s[id - 1]);
if(wh + 1 <= r) out(wh + 1, r, reba[wh + 1][r][id]);
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1;i <= m;i ++) cin >> a[i] >> b[i] >> c[i], s.push_back(c[i]);
sort(s.begin(), s.end());
s.erase(unique(s.begin(), s.end()), s.end());
for(int i = 1;i <= m;i ++) c[i] = lower_bound(s.begin(), s.end(), c[i]) - s.begin() + 1;
for(int l = n;l >= 1;l --)
{
for(int r = l;r <= n;r ++)
{
for(int k = l, sum;k <= r;k ++)
{
init(l, r, k);
sum = 0;
for(int i = s.size(), ans;i >= 1;i --)
{
sum += cnt[i];
ans = sum * s[i - 1] + g[l][k - 1][i] + g[k + 1][r][i];
if(ans > f[l][r][i] || !f[l][r][i])
{
f[l][r][i] = ans;
ba[l][r][i] = k;
}
}
}
for(int k = s.size();k >= 1;k --)
{
if(f[l][r][k] > g[l][r][k + 1] || !g[l][r][k + 1])
{
g[l][r][k] = f[l][r][k];
reba[l][r][k] = k;
}
else {
g[l][r][k] = g[l][r][k + 1];
reba[l][r][k] = reba[l][r][k + 1];
}
}
}
}
printf("%d\n", g[1][n][1]);
out(1, n, reba[1][n][1]);
return 0;
}

AcWing-321-棋盘分割

先来一波推导。
ans2=i=1n(xix)n =1n(i=1n(xi22xix+x2))

=1n(i=1nxi22xi=1nxi+nx2)

=i=1nxi2nx2

本题的划分分为横向和纵向,同时区间也由一维扩展到了二维,因此实现时可以用记忆化搜索的方式减少代码量。用fx1,y1,x2,y2,k表示(x1,y1)(x2,y2)这个二维区间分割k次的最小均方差,那么这个区间可以横向或纵向划分,且可以任意选取划分后的两个子区间,按照这个逻辑,可以写出下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>
#include <iomanip>
#include <cmath>
#include <cstring>

const int m = 8, M = 9, N = 20, PosInf = 1e9;

int n, s[M][M];
double f[M][M][M][M][N];
double average;

inline double Calc(int x1, int y1, int x2, int y2)
{
double sum = s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1];
return (sum - average) * (sum - average) / n;
}

inline double Search(int x1, int y1, int x2, int y2, int k)
{
double &v = f[x1][y1][x2][y2][k];
if (v >= 0)
return v;
if (k == 1)
return v = Calc(x1, y1, x2, y2);

v = PosInf;
for (int i = x1; i < x2; i++)
{
v = std::min(v, Search(x1, y1, i, y2, k - 1) + Calc(i + 1, y1, x2, y2));
v = std::min(v, Search(i + 1, y1, x2, y2, k - 1) + Calc(x1, y1, i, y2));
}
for (int i = y1; i < y2; i++)
{
v = std::min(v, Search(x1, y1, x2, i, k - 1) + Calc(x1, i + 1, x2, y2));
v = std::min(v, Search(x1, i + 1, x2, y2, k - 1) + Calc(x1, y1, x2, i));
}

return v;
}

int main()
{
std::cin >> n;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= m; j++)
{
std::cin >> s[i][j];
s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
}

memset(f, -1, sizeof(f));
average = (double)s[m][m] / n;

std::cout << std::fixed << std::setprecision(3) << sqrt(Search(1, 1, 8, 8, n)) << std::endl;
return 0;
}

洛谷-P1436-棋盘分割

这个跟上面一题其实是一样的,经过数学推导,上题求均方差,也就是求平方和,但均方差可以直接求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <iostream>
#include <cstring>

const int m = 8, M = 9, N = 20, PosInf = 1e9;

int n, s[M][M];
double f[M][M][M][M][N];

int Calc(int x1, int y1, int x2, int y2)
{
double sum = s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1];
return sum * sum;
}

double Search(int x1, int y1, int x2, int y2, int k)
{
double &v = f[x1][y1][x2][y2][k];
if (v >= 0)
return v;
if (k == 1)
return v = Calc(x1, y1, x2, y2);

v = PosInf;
for (int i = x1; i < x2; i++)
{
v = std::min(v, Search(x1, y1, i, y2, k - 1) + Calc(i + 1, y1, x2, y2));
v = std::min(v, Search(i + 1, y1, x2, y2, k - 1) + Calc(x1, y1, i, y2));
}
for (int i = y1; i < y2; i++)
{
v = std::min(v, Search(x1, y1, x2, i, k - 1) + Calc(x1, i + 1, x2, y2));
v = std::min(v, Search(x1, i + 1, x2, y2, k - 1) + Calc(x1, y1, x2, i));
}

return v;
}

int main()
{
std::cin >> n;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= m; j++)
{
std::cin >> s[i][j];
s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
}

memset(f, -1, sizeof(f));

std::cout << Search(1, 1, 8, 8, n) << std::endl;
return 0;
}

区间型动态规划的两种实现方式

迭代式(推荐)

1
2
3
4
5
6
7
for (int len = 1; len <= n; len++)
for (int l = 1; l + len - 1 <= n; l++)
{
r = l + len - 1;
for (int k = l; k < r; k++)
// do something
}

记忆化搜索式(适合状态定义较为复杂的情况)

如果状态定义中数组维数太多,需要些很多重循环,这时可以用记忆化搜索的形式来写。

2D/1D 的四边形不等式优化

2D/1D dp 的方程模型如下:
d[i,j]=minl(i,j)kr(k,j)(d[i,k]+d[k+1,j]+val(i,j))

其中 2D 指枚举的状态数, 1D 指枚举的决策数,最典型的是区间 dp ,明显,直接 dp 时间为 O(n3) (一定要注意 val(i,j)k 无关)

警告!下面的证明都很繁琐且没啥用(完全不影响做题),可以只记结论

2D 状态四边形不等式判定

对于 2D/1D dp 的方程:
d[i,j]=minl(i,j)kr(k,j)(d[i,k]+d[k+1,j]+val(i,j))
val 满足四边形不等式区间包含单调性d 满足 d[i,i]=val(i,i)=0 ,且 il(i,j)r(i,j)<jd[i,j] 满足四边形不等式

证明(又臭又长):

j=i+1 时, d[i,j+1]+d[i+1,j]=d[i,i+2]+d[i+1,i+1] ,又因为 d[i+1,i+1]=0 ,故 d[i,j+1]+d[i+1,j]=d[i,i+2]

由于 il(i,j)r(i,j)<jd[i,i+2] 的决策最多只有 ii+1

  1. d[i,i+2] 的最优决策为 i+1 ,则
    d[i,i+2]=d[i,i+1]+d[i+2,i+2]+val(i,i+2)=d[i,i+1]+val(i,i+2)
    ,发现 d[i,i+1] 的决策只有 i ,故 d[i,i+1]=d[i,i]+d[i+1,i+1]+val(i,i+1)=val(i,i+1) ,综合两式,有 d[i,i+2]=val(i,i+1)+val(i,i+2)=d[i,j+1]+d[i+1,j]

    同样因为 d[i,i]=0 ,有 d[i,i+1]+d[i+1,i+2]=val(i,i+2)+val(i+1,i+2)=d[i,j]+d[i+1,j+1]

    由于 val 满足四边形不等式,有 val(i,i+1)+val(i,i+2)val(i,i+1)+val(i+1,i+2)

    d[i,j+1]+d[i+1,j]d[i,j]+d[i+1,j+1] ,即 d 满足四边形不等式

  2. d[i,i+2] 的最优决策为 i ,同理有
    d[i,i+2]=d[i,i]+d[i+1,i+2]+val(i,i+2)=d[i+1,i+2]+val(i,i+2)=val(i+1,i+2)+val(i,i+2)
    d[i+1,i+2]+d[i,i+1]=val(i+1,i+2)+val(i,i+1)=d[i+1,j+1]+d[i,j]

    同样由 val 满足四边形不等式,得 d[i,j+1]+d[i+1,j]d[i,j]+d[i+1,j+1]

综上,当 j=i+1 ,即 ji=1 时, d 满足四边形不等式

假设当 ji<k 时四边形不等式成立,下面证明 ji=k 时成立

d[i,j+1] 最优决策为 xd[i+1,j] 最优决策为 y

有等式 (1)
d[i,j+1]+d[i+1,j]=d[i,x]+d[x+1,j+1]+val(i,j+1)+d[i+1,y]+d[y+1,j]+val(i+1,j)
而对于 d[i,j]d[i+1,j+1]x,y 就不一定是最优决策了

xy 时,取 xd[i,j] 的决策, yd[i+1,j+1] 的决策,由不优得不等式 (2)
d[i,x]+d[x+1,j]+val(i,j)+d[i+1,y]+d[y+1,j+1]+val(i+1,j+1)d[i,j]+d[i+1,j+1]
(1)+(2)(3)
d[i,x]+d[x+1,j]+val(i,j)+d[i+1,y]+d[y+1,j+1]+val(i+1,j+1)+d[i,j+1]+d[i+1,j]d[i,j]+d[i+1,j+1]+d[i,x]+d[x+1,j+1]+val(i,j+1)+d[i+1,y]+d[y+1,j]+val(i+1,j)d[x+1,j]+d[i+1,y]+d[y+1,j+1]+d[i,j+1]+d[i+1,j]+val(i,j)+val(i+1,j+1)d[i,j]+d[i+1,j+1]+d[x+1,j+1]+d[y+1,j]+val(i,j+1)+val(i+1,j)
x+1y+1j<j+1 ,由归纳假设,有 d[x+1,j+1]+d[y+1,j]d[x+1,j]+d[y+1,j+1] ,结合 val 满足四边形不等式,与 (3) 比较得:
d[i,j+1]+d[i+1,j]d[i,j]+d[i+1,j+1]
ji=k 时满足四边形不等式;

xy 时,取 yd[i,j] 的决策, xd[i+1,j+1] 的决策,同理可证得 ji=k 时满足四边形不等式

由数学归纳法原理,原命题得证

QED

2D 决策单调性定理

对于 2D/1D dp 的方程:
d[i,j]=minl(i,j)kr(k,j)(d[i,k]+d[k+1,j]+val(i,j))
d[i,j] 的最优决策点为 bp[i,j] ,若 i<jbp[i,j1]bp[i,j]bp[i+1,j] ,则称 d 具有决策单调性

d 满足 d[i,i]=val(i,i)=0il(i,j)r(i,j)<j ,且 d 满足四边形不等式,则 d 具有决策单调性

证明:

p=bp[i,j] ,则 i<kp ,因为 d 满足四边形不等式,有:
d[i,p]+d[i+1,k]d[i,k]+d[i+1,p]d[i+1,k]d[i+1,p]d[i,k]d[i,p]
又由 p 的最优性(带 dp 方程消去 val 可得):
d[i,k]+d[k+1,j]d[i,p]+d[p+1,j]d[i,k]d[i,p]+d[k+1,j]d[p+1,j]0
考虑对于 d[i+1,j]k 为决策减用 p 为决策:
(d[i+1,k]+d[k+1,j]+val(i+1,j))(d[i+1,p]+d[p+1,j]+val(i+1,j))=(d[i+1,k]d[i+1,p])+(d[k+1,j]d[p+1,j])(d[i,k]d[i,p])+(d[k+1,j]d[p+1,j])0
也就是说,对于 d[i+1,j]p 比任意 kp 更优,故 bp[i+1,j]bp[i,j] ,同理可证明 bp[i,j1]bp[i,j]

QED

实现

2D/1D 的实现没有 1D/1D 那么花里胡哨,就是直接记录 bp ,对于 d[i,j] ,只在 bp[i,j1]kbp[i+1,j] 的范围枚举 k 维护 d,bp

时间也很好分析:
i=1n1j=i+1n(bp[i+1,j]bp[i,j1]+1)=i=1n1(bp[i+1,n]bp[1,ni]+ni)n2
故时间复杂度优化到了 O(n2)

你可能觉得似乎没有 1D/1D 优化的厉害,但其实, 1D/1D 状态数是 O(n) 的,四边形不等式把转移从 O(n) 优化到了 O(logn) (斜率优化在最简单的情况下可以优化到 O(1) ,最麻烦情况下只有 O(log2n) );而 2D/1D 状态数是 O(n2) 的,四边形不等式把转移从 O(n) 直接优化到 O(1) ,作为对转移的优化,已经非常优秀了(事实上,如果你不考虑重新设计状态/排除无用状态, O(n2) 已经是最好复杂度了)

最后提醒一点:以上的复杂度分析都没有考虑计算 val 的复杂度!(这一点从例题十中也能看出)

例题十一

一个古老的石头游戏

题意同“石子合并”,但要求 O(n2) ,太经典了,就是板子

话说好像有 O(nlogn) 做法,但那和我们要讲的没关系了……

艹,好像数据加强了, O(n2) 过不了了! shit !

思路

呃呃,还要思路吗?

写个方程吧:设 d[i,j] 表示“合并区间 [i,j] 的最小代价”,转移有:
d[i,j]=minik<j(d[i,k]+d[k+1,j])+t=ijat
val(i,j) 显然可以前缀和做到 O(1)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <bits/stdc++.h>
using LL = long long;
const int N = 5000 + 5, INF = 1e9 + 5;
int n, a[N], d[N][N], bp[N][N];
LL sum[N];
int main()
{
for (; scanf("%d", &n), n; sum[0] = 0, printf("%d\n", d[1][n]))
{
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]), sum[i] = sum[i - 1] + a[i], d[i][i] = 0, bp[i][i] = i;
for (int len = 2; len <= n; ++len)
for (int i = 1, j, k; i <= n; ++i)
for (d[i][j = i + len - 1] = INF, k = bp[i][j - 1]; k <= bp[i + 1][j] && k < j; ++k)
if (d[i][k] + d[k + 1][j] + sum[j] - sum[i - 1] < d[i][j]) d[i][j] = d[i][bp[i][j] = k] + d[k + 1][j] + sum[j] - sum[i - 1];
}
return 0;
}