P1792 [国家集训队]种树
题面
前言
启初,我是不想写这个题的,因为蒟蒻我不会写链表 (好像就没写过几次)。
最后,还是在教练的鼓励(压迫) 下,恶补了一下关于链表的知识,才勉强过了这道题。
(毕竟去年csp D1T3就用到了链表,怎么也得学一下吧)
题解
我们一开始可能会想到 dp 但这题 \(n\) 的范围很明显不允许 dp通过的(但有大佬拿 WQS 二分加dp 强艹过去了 %%%)
实际上这是一个很妙的贪心。
我们正常的贪心策略就是先选大的。
可你会发现这种策略会被 Hack 掉的,连样例都过不去。
既然正常的贪心思路不行,我们搞一下带反悔的贪心。
因为我们选了一个数,与他相邻的都不会被选上。
也就是说,当我们选了一个数,当且仅当左右两边的数的和小于这个数。
那我们可以选了这个数之后,在把这个数的值设为他左右两边树的和与这个数的差值。
如果这个差值再次被选上,就意味着我们要选他左右两边的数,把中间的那个数舍弃。
所以,我们要搞个数据结构支持左右两边删数,链表完全可以胜任这份工作。
由于,本人初学链表,画的图不对,请大佬见谅QAQ
我们对于每个块维护一个左右指针,代表他左右两边的数。
假设,我们要删除五号左右两边的元素。
我们就要把他左指针所指元素的左指针所指的元素也就是三号元素的右指针指向五号元素。
我们再把他右指针的右指针所指的元素也就是七号元素的左指针指向五号元素。
那么五号元素的左指针就指向的是三,右指针就指向七。
我们就实现了链表的删除操作。
代码如下:
int x = L[L[t]], y = R[R[t]];// x代表他左指针的左指针所指的元素,y代表他右指针的右指针所指的元素
R[x] = t; L[t] = x;//把 t号点的左指针指向x,把x这个点的右指针指向t
L[y] = t; R[t] = y;//把 t号点的左指针指向y,把y这个点的左指针指向t
我们就可以拿链表和堆来维护这个过程。
代码如下:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
int n,m,step,ans,a[200010],used[200010],L[200010],R[200010];
inline int read()
{
int s = 0,w = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();}
while(ch >= '0' && ch <= '9'){s = s * 10 + ch - '0'; ch = getchar();}
return s * w;
}
priority_queue<pair<int,int>,vector<pair<int,int> > >q;
int main()
{
n = read(); m = read();
if(m * 2 > n)//无解的情况
{
puts("Error!");
return 0;
}
for(int i = 1; i <= n; i++)
{
a[i] = read();
L[i] = i - 1;
R[i] = i + 1;
q.push(make_pair(a[i],i));//一开始把所有的点都放入大根堆中
}
R[n] = 1; L[1] = n;
for(int i = 1; i <= m; i++)
{
int t = q.top().second; q.pop();
while(used[t])//如果这个数被删去了就需要在重新从堆中拿出一个数
{
t = q.top().second;
q.pop();
}
ans += a[t];//加上选他的贡献
a[t] = a[L[t]] + a[R[t]] - a[t];//重新设一下他的权值
used[L[t]] = 1; used[R[t]] = 1; //把他的左右两边标记为已经删过了
int x = L[L[t]], y = R[R[t]];//链表删除操作
R[x] = t; L[t] = x;
L[y] = t; R[t] = y;
q.push(make_pair(a[t],t));
// R[L[t]] = t; L[R[t]] = t;
}
printf("%d\n",ans);
return 0;
}
其实吧,链表这东西会用就行,到时候考场上画一画图就出来了。