BZOJ2151 种树 [贪心+链表]
Description
A城市有一个巨大的圆形广场,为了绿化环境和净化空气,市政府决定沿圆形广场外圈种一圈树。
园林部门得到指令后,初步规划出n个种树的位置,顺时针编号1到n。并且每个位置都有一个美观度Ai,如果在这里种树就可以得到这Ai的美观度。但由于A城市土壤肥力欠佳,两棵树决不能种在相邻的位置(i号位置和i+1号位置叫相邻位置。值得注意的是1号和n号也算相邻位置!)。
最终市政府给园林部门提供了m棵树苗并要求全部种上,请你帮忙设计种树方案使得美观度总和最大。如果无法将m棵树苗全部种上,给出无解信息。
Input
输入的第一行包含两个正整数n、m。第二行n个整数Ai。
Output
输出一个整数,表示最佳植树方案可以得到的美观度。如果无解输出“Error!”,不包含引号。
Sample Input
7 3
1 2 3 4 5 6 7
Sample Output
15
Hint
对于全部数据:m<=n; -1000<=Ai<=1000; n<=200000;
思路
这道题目看上去很容易想到是DP。实际上却是一种神贪心。。。都是套路
每次贪心选取最大值,答案加上这个最大值,最大值左右两边当然不能再选,于是就标记为访问过。最大值不难想到我们可以用堆进行维护。
但是这个贪心显然是不完善的。当最大值左右两边之和大于它本身时,就会产生错误。于是就有一种神奇的操作:将左右两边之和减去最大值,作为新的节点,覆盖掉原来的最大值,并删去左右两个节点。
原因是要么选最大值,要么选它的左右之和,选最大值时答案加上这个最大值,不再去选新的节点,选左右之和时答案还是加上最大值,只不过后面还要加上新节点的值。也就是当我们再次选取的新节点时,也就相当于选取当时最大值的左右之和,而没有选最大值。
这个神奇的操作有删除左右节点的过程,因此我们要用双向链表进行维护。总的来说这个思路比较难想到,但是代码还是比较好打的。
代码
需要注意的细节:
1.链表时环状的,第一个节点与最后一个相邻
2.进行删除节点时要注意前驱和后继都要修改
3.操作的顺序要注意,先更新值再进行删除
#include <bits/stdc++.h>
#define MAXN 200005
int n, m, ans, num[MAXN], pre[MAXN], next[MAXN];
bool vis[MAXN];
struct node
{
int v, p;
bool operator < (const node &x) const
{
return v<x.v;
}
};
std::priority_queue <node> q; //STL优先队列,建立大根堆
int main()
{
scanf("%d%d",&n,&m);
if(m*2>n) {printf("Error!\n"); return 0;} //特判无解情况
for(int i=1; i<=n; i++)
{
scanf("%d",&num[i]);
q.push((node){num[i], i});
pre[i]=i-1; next[i]=i+1; //数组模拟链表
}
pre[1]=n; next[n]=1;
for(int i=1; i<=m; i++)
{
while(vis[q.top().p]) q.pop(); //访问过的直接出队
node top=q.top(); q.pop();
ans+=top.v;
vis[next[top.p]]=vis[pre[top.p]]=1; //将左右节点标记为访问过的
num[top.p]=num[pre[top.p]]+num[next[top.p]]-num[top.p];
//删除左右节点
pre[top.p]=pre[pre[top.p]];
next[top.p]=next[next[top.p]];
next[pre[top.p]]=top.p;
pre[next[top.p]]=top.p;
q.push((node){num[top.p], top.p});
}
printf("%d\n",ans);
}