P4971 断罪者
题目描述
人们的罪恶值\(E\)由人们生前所做过的事和他的死亡方式来决定。他们做过的坏事都会有一个罪恶值,这些坏事有可能会并入同一个集合,一个集合的罪恶值为该集合中罪恶值最大的坏事的罪恶值,而他们一生做过的事会互相影响,我们将他们生前做过的事分为4种,而最后的罪恶值\(E\)由其中所有集合的罪恶值的和决定。
1.做坏事——有罪恶值,单独为一集合。
2.做好事——将一件坏事的罪恶值清零。
3.忏悔——将指定集合中,最大罪恶值的事罪恶值减少。
4.认清自己——将两个坏事集合合并。
而死亡方式可分为 自然死亡 、事故死亡 和 自杀 。
1.自然死亡,没什么影响。
2.事故死亡,可以免除最大罪恶的坏事集合。
3.自杀,最大的坏事集合罪恶值翻倍。
输入格式
第一行三个输入 \(T\) \(W\) \(K\),代表有 \(T\) 个人等待断罪,\(W\) 为死亡方式,与描述序号对应,\(K\) 含义见输出格式。
接下来的\(T\)组数据, 每组数据第一行两个输入 \(n\) \(m\),代表他做过的坏事数量和其他事的数量。
第二行\(n\)个输入,代表每件坏事的罪恶值。
第\(3\)到第\(m+2\)行,每行有三种输入可能。(请联系题目描述进行理解)
\(2\) \(A\) 表示做好事,将坏事 \(A\) 罪恶值清零 。
\(3\) \(A\) \(B\) 表示忏悔,指定集合为 \(A\) 所在的集合,最大罪恶值的事减少 \(B\),若最大罪恶值比 \(B\) 小,则最大罪恶值的事罪恶值清零。对于罪恶值相等的坏事,认为编号更小的更坏。
\(4\) \(A\) \(B\) 表示认清自己,将 \(B\) 所在集合与 \(A\) 所在的集合合并。
输出格式
对于每一个人,一行输出
若他的罪恶值为 \(0\) 则输出 \(Gensokyo\),
若他的罪恶值大于 \(K\) 则输出 \(Hell\),
否则输出 \(Heaven\)
再输出它的罪恶值
样例 #1
样例输入 #1
1 1 10
5 2
1 2 3 4 5
2 3
4 2 4
样例输出 #1
Heaven 10
样例 #2
样例输入 #2
2 2 8
5 4
4 8 7 5 6
4 2 4
2 2
4 2 3
3 3 2
3 2
5 1 2
2 2
3 3 2
样例输出 #2
Hell 9
Gensokyo 0
样例 #3
样例输入 #3
2 1 15
5 4
1 2 3 4 5
4 2 3
3 2 100
4 1 4
4 4 1
5 4
1 2 3 4 5
3 2 15
4 2 3
4 1 4
4 3 4
样例输出 #3
Heaven 11
Heaven 9
提示
样例解释与说明
样例一
一开始有五件坏事,罪恶值分别为 \(1.2.3.4.5\),做好事之后,罪恶值分别为 \(1.2.0.4.5\),认清自我后,只剩下四个集合,罪恶值分别是 \(1.4.0.5\),由于是自然死亡,所以最后的罪恶值 \(E=1+4+5=10 \le K \&\& E!=0\),因此输出 \(Heaven\)
样例二
对于样例2的第一组输入如下图,黑色椭圆代表一个集合,红色为罪恶值,下面为点的编号,由于是事故死亡,可以免去标号5的最大值,故罪恶值为\(E=4+5\)

说明
所有数据均在长整型范围内,对于所有数据,均有\(m\le n\),\(1\le K\),保证输入不存在负数。
由于读入数据可能会很大,建议使用较快的读入。
约定① 对于合并两个集合的操作,至少有一个集合只有一件坏事
约定② 这群人不会做好事
| 测试点编号 | T | n | 时限 | 约定 |
|---|---|---|---|---|
| 1 | \(\le10\) | \(\le100\) | \(1s\) | ①② |
| 2 | \(\le10\) | \(\le300\) | \(1s\) | ① |
| 3 | \(\le10\) | \(\le500\) | \(1s\) | |
| 4 | \(\le20\) | \(\le1000\) | \(1s\) | ①② |
| 5 | \(\le20\) | \(\le3000\) | \(1s\) | ① |
| 6 | \(\le20\) | \(\le7000\) | \(1s\) | |
| 7 | \(\le30\) | \(\le10000\) | \(1s\) | ①② |
| 8 | \(\le30\) | \(\le30000\) | \(1s\) | ① |
| 9 | \(\le30\) | \(\le50000\) | \(1s\) | |
| 10 | \(\le30\) | \(\le70000\) | \(1s\) | ①② |
| 11 | \(\le10\) | \(\le100000\) | \(1s\) | ① |
| 12 | \(\le10\) | \(\le150000\) | \(1s\) | |
| 13 | \(\le10\) | \(\le200000\) | \(1s\) | ①② |
| 14 | \(\le10\) | \(\le500000\) | \(1s\) | ① |
| 15 | \(\le10\) | \(\le1000000\) | \(2s\) | |
| 16 | \(\le10\) | \(\le1000000\) | \(2s\) | ①② |
| 17 | \(\le10\) | \(\le1000000\) | \(2s\) | ① |
| 18 | \(\le10\) | \(\le2000000\) | \(2s\) | |
| 19 | \(\le10\) | \(\le2000000\) | \(2s\) | |
| 20 | \(\le10\) | \(\le2000000\) | \(2s\) | |
| 21 | \(1\) | \(\le2000000\) | \(2s\) | 路径压缩 |
分析
集合,最大值,合并等关键词,可以想到用左偏树来维护。
对于修改任一节点,将其左右儿子合并,再与原左右儿子所在的堆合并即可。
注意修改时需要自底向上更新 \(dist\),不满足左偏树性质时则交换左右节点。
代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
int T,n,m,w,k,opt,a,b,sum,maxx;
bool vis[2000005];
struct node{
int dist,ch[2],val,id,fa;
bool operator<(node &x)const{return val==x.val?id>=x.id:val<x.val;}//重载运算符
}t[2000005];
int find(int x){return t[x].fa==x?x:t[x].fa=find(t[x].fa);}//用路径压缩的方法把找到当前堆根的时间复杂度降到 O(nlogn)
int& rs(int x){return t[x].ch[t[t[x].ch[0]].dist<t[t[x].ch[1]].dist];}//返回右儿子节点
void pushup(int x){
if(!x)return;
if(t[x].dist^(t[rs(x)].dist+1))t[x].dist=t[rs(x)].dist+1,pushup(t[x].fa);//自底向上更新dist
}
int merge(int x,int y){
if((!x)||(!y))return x|y;
if(t[x]<t[y])swap(x,y);
t[rs(x)=merge(rs(x),y)].fa=x;//基础合并操作
pushup(x);return x;//更新dist
}
signed main(){
cin>>T>>w>>k;
for(int i=1;i<=T;i++){
memset(vis,0,sizeof vis);
cin>>n>>m;sum=maxx=0; //注意初始化!!!
t[0].dist=t[0].ch[0]=t[0].ch[1]=t[0].val=t[0].id=t[0].fa=0;
for(int j=1;j<=n;j++)cin>>t[j].val,t[j].id=t[j].fa=j,t[j].ch[0]=t[j].ch[1]=t[j].dist=0;
for(int j=1;j<=m;j++){
cin>>opt>>a;
if(opt==2){
int q=0;
t[a].val=0;
q=t[t[a].ch[0]].fa=t[t[a].ch[1]].fa=merge(t[a].ch[0],t[a].ch[1]);
q=find(q);
t[a].ch[0]=t[a].ch[1]=t[a].dist=0;
a=find(a);
t[a].fa=t[q].fa=merge(q,a);
}
if(opt==3){
cin>>b;a=find(a);
int q=0;
t[a].val=max(t[a].val-b,0ll);
q=t[t[a].ch[0]].fa=t[t[a].ch[1]].fa=merge(t[a].ch[0],t[a].ch[1]);
q=find(q);
t[a].ch[0]=t[a].ch[1]=t[a].dist=0;
a=find(a);
t[a].fa=t[q].fa=merge(q,a);
}
if(opt==4){
cin>>b;a=find(a),b=find(b);
if(a^b)t[a].fa=t[b].fa=merge(a,b);
}
}
for(int j=1;j<=n;j++){//查询直接模拟即可
int x=find(j);
if(!vis[x]){
vis[x]=1;
maxx=max(maxx,t[x].val),sum+=t[x].val;
}
}
if(w==2)sum-=maxx;
if(w==3)sum+=maxx;
if(!sum)puts("Gensokyo 0");
else if(sum<=k)printf("Heaven %lld\n",sum);
else printf("Hell %lld\n",sum);
}
return 0;
}
几个坑点
我全都踩过啊qwq
-
多测注意初始化!
-
注意修改值之后记得与原左右儿子的堆合并,并不是把它永久删除!
-
我的
merge函数中没有找 \(x,y\) 的堆的根节点,所以注意要保证合并时 \(x,y\) 已经是堆的根节点(即在merge之前find,详情见代码)! -
注意是值相同时,编号越小的越“大”,所以重载运算符时需要搞清楚符号!

浙公网安备 33010602011771号