题目链接:http://lx.lanqiao.cn/problem.page?gpid=T576
炫炫回了内蒙,肯定要吃羊肉啦,所有他家要宰羊吃。
炫炫家有 N 只羊,羊圈排成一排,标号 1 ~ N。炫炫每天吃掉一只羊(这食量!其实是放生啦),吃掉的羊的邻居会以为它被放生了,然后又会告诉他们的邻居,这样一直传播下去,除非某个邻居已经被“放生”了。每一天,所有知道某羊被“放生”了这个消息的羊都会很不满,如果不给他们巧克力的话,他们就会很造反,炫炫已经知道他要吃掉哪些羊,他可以任意安排吃的顺序,然后使巧克力的用量最小,请求出这个最小值。
本题有多组数据,第一行为数据组数 T。
对于每组数据:
第一行:两个用空格隔开的整数:N 和 M,表示羊的数量和需要吃掉的数量;
第二行:有 M 个数,表示要吃那些羊。
T 行,为每组数据的答案。
2
8 1
3
20 3
3 6 14
T = 10
N <= 10000
M <= 100
这题可用 区间dp 解决。
1. 首先我们来看(重温)dp的三个性质:
最优子结构
重叠子问题
无后效性
2. 回到题目中:
首先给出状态转移方程:
$$ dp[l][r] = min\{ \, dp[l][r], \,\, dp[l][i - 1] + dp[i + 1][r] + (arr[r + 1] - arr[l - 1] - 2) \, \} $$
数组 $arr[i]$ 存储被杀羊的号码,而 $i$ 表示羊在数组 $arr$ 中的编号,例如在第2个样例中,一共有3只羊被杀,羊的号码分别为$3,6,14$ ,则数组存储的规则为:$arr[1]=3,arr[2]=6,arr[3]=14$。
$dp[l][r]$ 表示杀掉 $arr[l] \sim arr[r]$ 只羊所花费的最小代价。这个表达可能有些拗口,举个栗子:$dp[1][3]$ 表示杀掉数组编号为 $1,2,3$ 的羊花费的最小代价,即 $arr[1],arr[2],arr[3]$ 这三只羊。
3. 为什么要用 dp 求解:
再来看一下题意,题目有两点关键信息:
我们可以看出,其实求 $dp[l][r]$ 可以缩小问题规模,变成求 $dp[l][k] + dp[k + 1][r]$ 的最小代价,对吧?如果你认为没问题,好,通过不断地拆分区间求解,问题的规模得到了缩小,这里符合dp基本性质之一“最优子结构”。
而我们知道,在动态规划算法中,利用问题的最优子结构性质,是以自底向上的方式递归地从子问题的最优解逐步构造出整个问题的最优解。
在本题中,从子问题 → 整个问题的求解,可以这样做,区间以每只即将被杀掉的羊为结点划分:
在本题中,从区间的角度,我们要求的最小代价 $dp[1][m]$ ,可以表示成:
即杀掉所有羊的代价可以变成求杀掉 $arr[1] \sim arr[k - 1]$ 只羊的最小代价与 $arr[k + 1] \sim arr[m]$ 只羊,再加上杀掉 $arr[k]$ 的最小代价。
好了,到这一步应该没有太难理解的地方,那么应该如何求杀掉 $arr[k]$ 的最小代价?
4. 如何求杀掉 $arr[k]$ 的最小代价::
通过上面列出的 dp 方程中可以看到
$$ arr[k]=arr[r + 1] - arr[l - 1] - 2 $$
为啥?
我们改一下第 1 个样例中的数据,再举个栗子:
羊 圈:1
2
3
45
6 7 8
待杀羊号码:2 3 5 $(arr[1]=2, arr[2]=3, arr[3]=5)$
考虑区间:{ $2$ },$2$ 表示待杀羊在数组中的编号。
即我们要杀掉号码为 $3$($arr[2]=3$)的这只羊,也就是要求杀掉 $arr[2]$ 的代价,这时候3号羊的邻居得到了消息,它们开始传播消息,什么时候传播停止?遇到了号码为2与号码为5的羊!
换句话说,我们在杀死号码为3的这只羊时,它的左右邻居肯定被影响,也就是区间外的羊也会被影响,而传播停止是在遇到被杀死的羊的时候。
再来看公式:
是将区间扩展到左右边界外第一只待杀死的羊,为啥是 $-2$ ?
2
, 3
, 4, 5
},就是例子中羊的号码为 $2$ 与号码为 $5$ 的位置,这两只羊是要被杀死的,此时区间中有 $5-2-1$ 只羊。接下来验证一下这个公式是否合理:
考虑区间:{ $2$ },此时区间长度为 $1$,区间左部 $l$ 与区间右部 $r$ 相等,所以 $l=r=2$(前面约定,区间是以每只即将被杀掉的羊为结点划分,在这个例子中,羊的结点编号为 $2$ )
代入公式:
公式结果表明,杀掉 $arr[2]$ 这只羊,最小代价是 $1$。
5. 考虑边界值:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 1e4 + 5;
const int INF = 0x3f3f3f;
int arr[maxn] = {0}, dp[maxn][maxn] = {0};
int main (){
//freopen("input.txt", "r", stdin);
int t;
cin >> t;
for(int x = 0;x < t;x++){
int n, m;
cin >> n >> m;
for(int i = 1;i <= m;i++){
cin >> arr[i];
}
//处理边界值
arr[0] = 0, arr[m + 1] = n + 1;
//区间长度由小到大
for(int len = 1;len <= m;len++){
//考虑区间情况 l → r
for(int l = 1;l + len <= m + 1;l++){
//计算区间的右边界 r
int r = l + len - 1;
//区间长度为 1 的特殊情况
if(len == 1){
dp[l][r] = arr[r + 1] - arr[l - 1] - 2;
}else{
//赋初值
dp[l][r] = INF;
//逐个遍历区间内每只待杀的羊
for(int i = l;i <= r;i++){
dp[l][r] = min( dp[l][r], dp[l][i - 1] + dp[i + 1][r] + arr[r + 1] - arr[l - 1] - 2 );
}
}
}
}
printf("%d\n", dp[1][m]);
}
return 0;
}
如果你还是有一些疑惑,建议在代码中进行调试,输出各数据的运行情况,这可能有利于你理解。
另外如果你想进一步了解区间 dp 原理,可以查看 $References$ 链接中的第一个。
有问题欢迎指正!