区间动态规划
区间动态规划框架
...
memset(dp, 0, sizeof(dp));
for(len = 2; len <= n; len++)//区间动态规划是把一个大区间划分成若干个小区间的方式。这句代表小区间长度
for(i = 1, j = len; j <= n; i++, j++)//枚举区间起点,划分[i, j]
for(k = 1; k < j; k++){
dp[i][j] = func(dp[i][j],...)
}
...
秒解变式石子儿
在圆形操场上摆放着一行共n堆的石子。现要将石子有序地合并成一堆。规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆石子数记为该次合并的得分。请编辑计算出将n堆石子合并成一堆的最小得分和将n堆石子合并成一堆的最大得分。
这一题和直线石子儿的区别是:直线石子儿是首尾不相连的,这个形成了一个闭环。起初,我想的解决这个题的方法是:把上面框架里的k增加一个(这种思想来源于高中生物必修三基因工程)。要把一个环切开,需要切两刀。因为之前对直线时设置k也就相当于设置一个断点。现在,切一下只能断开两端,是远远不够的。于是我尝试设置两个端点,划分成两个直线,再对每条直线求dp。
但随之而来的问题是,我根本写不出这样的实现。我推测可能是我思路错误或太复杂,于是换了一种思路。虽然这是一个环,但求得分后会产生一个计数缺口(原因是选用区间时就已经默认设置了首尾)。于是,应该可以把一个区间扩展成两个,比如一个以
3
,
1
,
2
3,1,2
3,1,2组成的环,可以像下面这样展开:
3
,
1
,
2
,
3
,
1
,
2
3,1,2,3,1,2
3,1,2,3,1,2
于是,得到的前缀和就是这样:
3
,
4
,
6
,
9
,
10
,
12
3,4,6,9,10,12
3,4,6,9,10,12
这样的前缀和,对环形是没有影响的。
这样,一个长度为n的数组会扩展成2n长度。因此,在框架中间选用
[
i
,
j
]
[i, j]
[i,j]的时候,要把
j
<
=
n
j<=n
j<=n该换成
j
<
=
2
∗
n
j<=2*n
j<=2∗n,同时,最后也不是cout出
d
p
[
1
]
[
n
]
dp[1][n]
dp[1][n],而是循环查出
[
1
,
n
]
[1, n]
[1,n]区间里的最大/最小值。这是第二行的改变产生的改变。第二行其实相当于一个“单向等距双指针”(我自己起的),就是检查[1, n]后再检查[2, n + 1]一直检查到[n + 1, 2n]。而单纯是输出dp[1][2 * n]会产生错误,因为[i, j]再进行区间选择的时候会卡在n的位置。因此,完整代码如下:
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
#define Max 1000
#define INF 0x3f
int n, sum[Max], dp1[Max][Max], dp2[Max][Max];
int main(){
while(~scanf("%d", &n)){
int minx = INF, maxx = 0;
sum[0] = 0;
memset(dp1, INF, sizeof(dp1));
memset(dp2, 0, sizeof(dp2));
for(int i = 1; i <= n; i ++){
scanf("%d", &sum[i]);
sum[i + n] = sum[i];
sum[i] += sum[i - 1];
dp2[i][i] = 0;
dp1[i][i] = 0;
}
for(int i = n + 1; i <= 2 * n; i++){
sum[i] += sum[i - 1];
dp1[i][i] = 0;
dp2[i][i] = 0;
}
for(int len = 2; len <= n; len++){
for(int i = 1, j = len; j <= 2 * n; i++, j++){
for(int k = i; k < j; k++){
dp1[i][j] = min(dp1[i][j], dp1[i][k] + dp1[k + 1][j] + sum[j] - sum[i - 1]);
dp2[i][j] = max(dp2[i][j], dp2[i][k] + dp2[k + 1][j] + sum[j] - sum[i - 1]);
}
}
}
for(int i = 1; i <= n; i++){
maxx = max(maxx, dp2[i][i + n - 1]);
minx = min(minx, dp1[i][i + n - 1]);
}
cout << minx << " " << maxx << endl;
}
return 0;
}
鲜有人做的L题
我看L题只有两个人做。也许在最中间,大家都以为“强迫症”去做两边儿的题。于是脑回路清奇的我偏挑这一题做。
题目的意思就是,有一组数字,每次可以从左边或者从右边取走连续的若干个数字,然后两个取,A先取,B后取,两个人都是尽量使自己比对方取的分数多,问最后A比B多多少。
这个题刚开始我还真的没思路。于是我最后只能去网上寻题解。主要是这还是个PDF格式的题,不好找。
不过我最终还是知道了方法:
首先我们用f[i][j]表示在取 i 到 j 范围内的数字,先取的人最大取到几,我们每次计算时就找它这个区间中间的连续区间f[i][k] f[k][j] (k从i到j) 然后把最小的值找出来,那么f[i][j]就等于这个区间所有数字的和减去这个最小值。
实现代码:
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
using namespace std;
const int N = 105;
int n, num[N], sum[N], f[N][N];
int main() {
while(scanf("%d", &n) && n != 0) {
sum[0] = 0;
for (int i = 1 ; i <= n ;i++) {
scanf("%d",&num[i]);
sum[i] = sum[i - 1] + num[i];
}
memset(f, 0, sizeof(f));
for (int i = 1; i <= n; i++) f[i][i] = num[i];
for (int i = 1; i <= n; i++)
for(int j = 1; i + j <= n; j++){
int m = 0, r = i + j, s = sum[i + j] - sum[j -1];
for (int k = j; k <= r ; k++) {
m = m < f[j][k] ? m : f[j][k];
m = m < f[k][r] ? m : f[k][r];
}
f[j][r] = s - m;
}
printf("%d\n", f[1][n] - (sum[n] - f[1][n]));
}
return 0;
}
总结
这周开了背包问题。此类的背包比贪心算法的背包问题在一定条件下更精确。其状态转移方程为:
f
[
i
]
[
v
]
=
m
a
x
(
f
[
i
−
1
]
[
v
]
,
f
[
i
−
1
]
[
v
−
c
[
i
]
]
+
w
[
i
]
)
f[i][v]=max(f[i-1][v],f[i-1][v-c[i]]+w[i])
f[i][v]=max(f[i−1][v],f[i−1][v−c[i]]+w[i])
还是很好理解的。
这周还做了一些区间DP的题,题少,但精炼,值得细细回味。