CF 311C
题目
\(h\) 个房间,其中 \(n\) 个有宝藏,第 \(i\) 个宝藏房编号为 \(a_i\),里面宝藏价值为 \(c_i\) 。初始位置为 \(1\),每次可以跨越 \(k\) 个房间,可以移动无数次。
有 \(m\) 次操作,形如:
\(1\) \(x\),表示增加一个技能,每次可以跨越 \(x\) 个房间
\(2\) \(x\) \(y\) ,表示让第 \(x\) 个房间宝藏价值减少 \(y\)
\(3\) ,查询当前从 \(1\) 出发能到达的所有房间中宝藏价值最大的房间,并 拿走 它的宝藏,如果有多个最大值,任意拿走一个。
数据范围:
\(h\leq 10^{18},n\leq 10^5,k\leq 10^4,a_i\leq h,c_i\leq 10^9\)
保证 \(1\) 操作不超过 \(20\) 次,\(2\) 操作不会减成负数,并且 \(2\) 操作不会减少已经拿走的宝藏。
题解
发现 \(k\) 很小,那么考虑将原序列分成若干个模 \(k\) 同余的组,那么组内可以从前往后到达,并且将序列中每 \(k\) 个分成一段。
那么现在发现每个组想要知道的信息仅为,该组最早能在哪一段被到达,因为前面的一个能走到,那么后面的都能走到了。
考虑每次增加的技能 \(x\) ,相当于可以从 第 \(i\) 组走到 \((i+x)\mod k\) 组,跨越了 \((i+x)/k\) 个段,那么每次增加技能我们都相当于从 \(i\) 向 \((i+x)\mod k\) 连一条权值为 \((i+x)/k\) 的段,每次都重新跑一遍最短路,就可以知道每一组最少走多少段被到达。每次新增的能到达的点直接加到 \(\text{set}\) 里就好了。
小细节:因为保证了 \(2\) 操作不会减少已经拿走的宝藏,那么 \(\text{set}\) 里面只维护值,二操作每次直接删就行了。不用维护标号。。刚开始被翻译坑了
const int N=2e5+5;
const int M=10050;
ll h,a[N];
int n,m,k,c[N],op,x,y;
multiset<pii>s;
vector<pii> v[M];
bool vis[N],pd[N],cd[N];
int dis[N];
void spfa(int tmp){
mem0(vis);
mem0x3f(dis);
queue<int>q;
q.push(tmp);dis[tmp]=0;
while(!q.empty()){
auto u=q.front();
q.pop();
vis[u]=0;
for(auto i:v[u]){
if(dis[i.fi]>dis[u]+i.se){
dis[i.fi]=dis[u]+i.se;
if(!vis[i.fi]){
q.push(i.fi);vis[i.fi]=1;
}
}
}
}
}
void solve(){
for(int i=0;i<k;i++){
v[i].pb(mp((i+x)%k,(i+x)/k));
}
spfa(1%k);
for(int i=1;i<=n;i++){
if(!pd[i] && dis[a[i]%k]<=(a[i]/k)){
pd[i]=1;
s.insert(mp(c[i],i));
}
}
}
signed main(){
#ifdef newbiewzs
#else
#endif
cin>>h;n=read();m=read();k=read();
for(int i=1;i<=n;i++){
a[i]=read();c[i]=read();
}
for(int i=1;i<=n;i++){
if(a[i]%k==1%k){
pd[i]=1;
s.insert(mp(c[i],i));
// cerr<<"sesese:"<<c[i]<<endl;
}
}
for(int i=1;i<=m;i++){
op=read();
if(op==1){
x=read();
solve();
}
if(op==2){
x=read();y=read();
if(pd[x]){
auto it=s.lower_bound(mp(c[x],x));
s.erase(it);
}
c[x]-=y;
if(pd[x]){
s.insert(mp(c[x],x));
}
}
if(op==3){
if(!s.size()){
puts("0");
}
else{
auto it=--s.end();
cout<<(*it).fi<<'\n';
cd[(*it).se]=1;
s.erase(it);
}
}
}
return 0;
}