洛谷 P3620 - P3622 数据备份、风铃、动物园(2007 APIO)
P3620 数据备份
题目描述:
思路:
我们先来将这道题简化一下:
将每一对相邻的建筑之间的空地看作一个点,点的权值即为两个建筑物之间的距离。那么题意可以转化为:
在这n - 1个点中选出两两不相邻的k个点,使得这k个点的权值之和最小。
这样简化完题意之后,我们就可以很轻松地解决这个问题了。
首先,我们来看一下一个错误思路。
错误思路:
直接将所有点存入小根堆中,每一次取出堆顶元素并将堆顶元素左右两边的元素删除,最后求得最大值。
我们举个例子就可以证明这种思路的错误性:
如下图,一共四片空地,k = 2。
按照上述思路,第一次会取出长度为1的空地,取出后并删除左右两边的点,那么就会变为:
最后只剩下一片长度为9的空地,那么显然得到ans = 10。但是我们直接观察都可以得到,正确答案应该为2 + 2 = 4(选择第一个和第3个空地)。
因此,我们需要一个反悔机制。
反悔机制的思路:
在取出堆顶元素后,在将其左右元素删除之前,先将这个点的权值置为它的左右两个点的权值之和减去这个点的权值,然后将这个新的权值放入小根堆中。
根据这个思路,还是用刚才的例子,在经过第一次操作后可以得到:
那么下一个堆顶元素就不是9,而是3了。
因此答案就是1 + 3 = 4 = 2 + 2,是正确的了。
那为什么这种思路是正确的呢?
因为这个“反悔机制”就相当于“选择了这个点左右两个点,而不选这个点”。用上面的例子来说,就是取了两个长度为2的点,而没有取长度为1的点(2 + 2 - 1)。
代码:
#include <iostream>
#include <algorithm>
#include <queue>
#include <cstring>
using namespace std;
const int N = 1e5 + 10, INF = 0x3f3f3f3f;
struct place {
//表示每两个建筑之间的空地
int dist, l, r; //dist表示两个建筑之间的距离,l表示左边的建筑编号,r表示右边的建筑编号
} p[N]; //p[i]表示第i个和第i + 1个建筑之间的空地距离
struct Node {
//在优先队列中表示两个建筑之间的空地
int val, id; //表示间隔距离和空地的序号
bool operator < (const Node &t) const {
//由于是小根堆,所以要重载小于号
return val > t.val;
}
};
priority_queue<Node> q; //小根堆
int n, k;
int ans; //记录答案
bool st[N]; //用来标记i号点有没有被删除过
void del(int i) {
//表示选取了i号空地,删除其左右两边的空地的操作
p[i].l = p[p[i].l].l, p[i].r = p[p[i].r].r;
p[p[i].l].r = i, p[p[i].r].l = i;
}
int main() {
int last;
scanf("%d%d%d", &n, &k, &last);
for (int i = 1 ; i < n ; i ++ ) {
int dist;
scanf("%d", &dist);
p[i].dist = dist - last;
last = dist;
p[i].l = i - 1;
p[i]