网址如下:
P1552 [APIO2012] 派遣 - 洛谷 | 计算机科学教育新生态
第一次通过洛谷里面的 省选/NOI- 难度的题耶
主要是为了练习昨天学的左偏树/可并堆的
思路还是挺清晰的:
按照题目的描述,忍者的上下级关系构建成了一颗树,我们可以选择一个结点作为管理人(根节点),然后可以派遣该子树下面的所有结点去做任务
很明显,派遣的忍者数越多越好,所以我们要尽可能选择该子树下的薪水比较少的结点
使用左偏树/可并堆可以解决这个问题,把该子树构建成按薪水比较的最小可并堆,然后不断选取最小的值来计算该子树的最大派遣的忍者数,当该子树的根节点的父节点想要构建最小可并堆的时候,就可以把子树的可并堆和父节点合并,达到增加效率的效果
代码如下:
#include<cstdio>
#include<algorithm>
#include<list>
#include<queue>
#include<vector>
using namespace std;
typedef long long LL;
const int maxn = 100010;
struct Heap{
LL C, L;//薪水,领导力
int l, r;//左、右结点
int fa;//并查集的上级
int dist;
}hp[maxn];
struct miniHode{
LL C;
int id;
miniHode(LL C, int id):C(C), id(id){}
bool operator<(const miniHode &t)const{return C > t.C;}
};
struct Node{
list<int> l;//子节点
}tr[maxn];
LL ans = 0;//最大满意度
int N;
LL M;
int find(int x){
if(hp[x].fa == x) return x;
return hp[x].fa = find(hp[x].fa);
}
int merge(int x, int y){
if(!x | !y) return x | y;
if(hp[x].C > hp[y].C) swap(x, y);
hp[x].r = merge(hp[x].r, y);
if(hp[hp[x].l].dist < hp[hp[x].r].dist) swap(hp[x].l, hp[x].r);
hp[x].dist = hp[hp[x].r].dist + 1;
hp[x].fa = hp[hp[x].l].fa = hp[hp[x].r].fa = x;
return x;
}
void get_heap(int x){
for(auto it = tr[x].l.begin(); it != tr[x].l.end(); it++){
get_heap(*it);
merge(find(x), find(*it));
}
int fx = find(x);
priority_queue<miniHode, vector<miniHode>> q; q.push(miniHode(hp[fx].C, fx)); LL n = 0, m = M;
while(!q.empty()){
miniHode u = q.top(); q.pop();
if(u.C > m) break;
if(hp[x].L * (m / u.C + n) <= ans) return;
m -= u.C; n++;
if(hp[u.id].l){
if(hp[u.id].r){
if(hp[hp[u.id].l].C < hp[hp[u.id].r].C){q.push(miniHode(hp[hp[u.id].l].C, hp[u.id].l)); q.push(miniHode(hp[hp[u.id].r].C, hp[u.id].r));}
else{q.push(miniHode(hp[hp[u.id].r].C, hp[u.id].r)); q.push(miniHode(hp[hp[u.id].l].C, hp[u.id].l));;}
}
else q.push(miniHode(hp[hp[u.id].l].C, hp[u.id].l));
}
else if(hp[u.id].r) q.push(miniHode(hp[hp[u.id].r].C, hp[u.id].r));
}
ans = max(ans, n * hp[x].L);
}
int main(void)
{
scanf("%d%lld", &N, &M);
for(int i = 1; i <= N; i++){
hp[i].fa = i;
int B;
scanf("%d%lld%lld", &B, &hp[i].C, &hp[i].L);
tr[B].l.push_back(i);
}
get_heap(1);
printf("%lld", ans);
return 0;
}
但是效率不够,有三个测试点TLE了,只有88分
这里可以引入贪心的思路,实际上不去构建最小可并堆,构建最大可并堆的效率更高
而这个最大可并堆要具有以下特征/功能:
1. 维护最大可并堆里的所有结点都是被派遣的忍者
2. 结点的数量需要被记录
3. 所有结点的薪水的总和需要被记录
效率更高的理由:
1. 结点数更小,在合并的时候更有优势一点(左偏树的合并因为有dist值进行优化,所以我只敢说一点)
2.计算客户满意度的时候,只需要把管理者的领导力(O(1))直接乘以记录的结点的数量(O(1))就可以了,而最小可并堆需要一个一个把薪水小的结点进行一个count(我是对堆用了一个优先队列(最小堆)来保证下一个count是薪水最小的结点,效率明显比O(1)慢,而且也比最大堆的维护慢)
而最大可并堆的维护代码如下:
while(hp[x].c > M){
hp[x].s--; hp[x].c -= hp[find(x)].C;
pop(find(x));
}
当总薪水超出预算的时候,就删除薪水最大的结点,直到总薪水不超出预算,这个时候派遣忍者数就是该子树最大的(贪心)
代码如下:
#include<cstdio>
#include<algorithm>
#include<list>
#include<queue>
#include<vector>
using namespace std;
typedef long long LL;
const int maxn = 100010;
struct Heap{
LL C, L;//薪水,领导力
LL s;//堆中忍者数
LL c;//费用和
int l, r;//左、右结点
int fa;//并查集的上级
int dist;
}hp[maxn];
struct Node{
list<int> l;//子节点
}tr[maxn];
LL ans = 0;//最大满意度
int N;
LL M;
int find(int x){
if(hp[x].fa == x) return x;
return hp[x].fa = find(hp[x].fa);
}
int merge(int x, int y){
if(!x | !y) return x | y;
if(hp[x].C < hp[y].C) swap(x, y);
hp[x].r = merge(hp[x].r, y);
if(hp[hp[x].l].dist < hp[hp[x].r].dist) swap(hp[x].l, hp[x].r);
hp[x].dist = hp[hp[x].r].dist + 1;
hp[x].fa = hp[hp[x].l].fa = hp[hp[x].r].fa = x;
return x;
}
void pop(int x){
hp[x].C = -1;
hp[hp[x].l].fa = hp[x].l; hp[hp[x].r].fa = hp[x].r;
hp[x].fa = merge(hp[x].l, hp[x].r);
}
void get_heap(int x){
for(auto it = tr[x].l.begin(); it != tr[x].l.end(); it++){
get_heap(*it);
hp[x].s += hp[*it].s;
hp[x].c += hp[*it].c;
merge(find(x), find(*it));
}
while(hp[x].c > M){
hp[x].s--; hp[x].c -= hp[find(x)].C;
pop(find(x));
}
ans = max(ans, hp[x].s * hp[x].L);
}
int main(void)
{
scanf("%d%lld", &N, &M);
for(int i = 1; i <= N; i++){
int B;
scanf("%d%lld%lld", &B, &hp[i].C, &hp[i].L);
tr[B].l.push_back(i);
hp[i].fa = i; hp[i].c = hp[i].C; hp[i].s = 1;
}
get_heap(1);
printf("%lld", ans);
return 0;
}