P3203 [HNOI2010]弹飞绵羊 —— 懒标记?分块?LCT?...FAQ orz

本文介绍了一道名为“弹飞绵羊”的算法题目,通过分块技术和LCT(链剖分)方法解决该问题。文章详细讲解了两种算法的具体实现过程,包括分块维护区间信息和使用LCT维护节点之间的父子关系。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

好久没写博客了哈,今天来水一篇。_(:з」∠)_

题目 :弹飞绵羊(一道省选题)

题目描述

某天,Lostmonkey发明了一种超级弹力装置,为了在他的绵羊朋友面前显摆,他邀请小绵羊一起玩个游戏。游戏一开始,Lostmonkey在地上沿着一条直线摆上n个装置,每个装置设定初始弹力系数ki,当绵羊达到第i个装置时,它会往后弹ki步,达到第i+ki个装置,若不存在第i+ki个装置,则绵羊被弹飞。绵羊想知道当它从第i个装置起步时,被弹几次后会被弹飞。为了使得游戏更有趣,Lostmonkey可以修改某个弹力装置的弹力系数,任何时候弹力系数均为正整数。

输入输出格式

输入格式:
第一行包含一个整数n,表示地上有n个装置,装置的编号从0到n-1。

接下来一行有n个正整数,依次为那n个装置的初始弹力系数。

第三行有一个正整数m,

接下来m行每行至少有两个数i、j,若i=1,你要输出从j出发被弹几次后被弹飞,若i=2则还会再输入一个正整数k,表示第j个弹力装置的系数被修改成k。

输出格式:
对于每个i=1的情况,你都要输出一个需要的步数,占一行。

输入输出样例

输入样例#1:
4
1 2 1 1
3
1 1
2 1 1
1 1

输出样例#1:
2
3
说明

对于20%的数据n,m<=10000,对于100%的数据n<=200000,m<=100000

分块艹法

分析(1)

首先,本人拿到这篇题目的时候脑子是没有转过来的。那时候我在想什么呢?。。。对,当我们修改了某个点的k值之后,那么这个操作对于后面的点来说是没有丝毫的影响的,但却会使其前面的指向它的节点造成影响(因为一开始我没用分块嘛,直接用了一个比较暴力的思想:ans存答案,来做这道题的),于是乎觉得这样做太暴力,然后就弄了个懒标记和染色(但是有点复杂的样子于是乎挂了)。然后就直接一个朴素的懒标记骗了个50,TLE 五个点。

代码如下。

#include<bits/stdc++.h>
using namespace std;
const int M=2e5+100;
inline int read(){
    int x=0; char c=getchar();
    while(!isdigit(c)) c=getchar();
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    return x;
}
int n,m,tag;
int k[M],ans[M];
inline void dfs(int to){  //更新区间内节点的ans值
    for(int i=tag-1;i>=to;--i)
        ans[i]=ans[i+k[i]]+1;
}
int main(){
    n=read();
    for(int i=0;i<n;++i)
        k[i]=read();
    for(int i=n-1;i>=0;--i){
        if(i+k[i]>=n) ans[i]=1;
        else ans[i]=ans[i+k[i]]+1;
    }
    m=read();
    while(m--){
        int op=read();
        if(op==1){
            int now=read();
            if(tag>now) dfs(now); //向前更新节点的ans值,直到当前的节点
            printf("%d\n",ans[now]);
        }
        else if(op==2){
            int now=read(),nwk=read();
            if(nwk==k[now]) continue;
            if(tag>now) dfs(now);  //原本的懒标记在后面那么先将now~tag的节点的ans值更新
            tag=now;    //懒标记记录下当前修改的位置
            k[now]=nwk; int to=now+k[now];
            if(to>=n) ans[now]=1;
            else ans[now]=ans[to]+1;
        }
    }
    return 0;
} 

那么我们先不进行分块解法的讨论,首先看看一道简单的分块题来熟(复)悉(习)一下分块这个算法吧。(如果你是初学,请点这里

Title :A Simple Problem with Integers

Description

You have N integers, A1, A2, ... , AN. You need to deal with two kinds of operations. One type of operation is to add some given number to each number in a given interval. The other is to ask for the sum of numbers in a given interval.
//概述一下,就是区间加以及区间求和(简直就是模板题),另外提一下这个东西也可以用线段树做

Input

The first line contains two numbers N and Q. 1 ≤ N,Q ≤ 1e5.
//表示有n(不超过1e5)个数字,Q(不超过1e5)个操作
The second line contains N numbers, the initial values of A1, A2, ... , AN. -1e9 ≤ Ai ≤ 1e9.
//第二行有n个数字,都是int/2的范围内的(但是加起来是会爆int的)
Each of the next Q lines represents an operation.
//表示接下来Q行是Q个操作
"C a b c" means adding c to each of Aa, Aa+1, ... , Ab. -1e4 ≤ c ≤ 1e4.
//C: 表示对a~b进行区间加操作
"Q a b" means querying the sum of Aa, Aa+1, ... , Ab.
//Q: 表示询问a~b的区间和

Output

You need to answer all Q commands in order. One answer in a line. //回答询问,每行一个答案

Sample Input

10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4
Sample Output

  • 4
    55
    9
    15
    Hint
  • The sums may exceed the range of 32-bit integers.
    //可能会爆int(就是要你开long long)

代码如下:

#include<iostream>
#include<cmath>
#include<cstdio>
#include<math.h>
typedef long long ll;
using namespace std;
const int M=1e5+100;
inline ll read(){
    ll x=0,f=1; char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    return x*f;
}
int n,q,block,num,l[M],r[M];
ll blg[M],a[M],d[M],sum[M];
inline void build(){ //建立分块
    block=sqrt((double)n);
    num=n/block; if(n%block) ++num; 
    for(int i=1;i<=num;++i)
        l[i]=(i-1)*block+1,r[i]=i*block;
    r[num]=n;
    for(int i=1;i<=n;++i)
        blg[i]=(i-1)/block+1,sum[blg[i]]+=a[i];
}
inline void update(int x,int y,int k){ //一个更新区间的操作
    if(blg[x]==blg[y]){
        sum[blg[x]]+=(y-x+1)*k;
        for(int i=x;i<=y;++i)
            a[i]+=k;
        return ;
    }
    sum[blg[x]]+=(r[blg[x]]-x+1)*k;
    sum[blg[y]]+=(y-l[blg[y]]+1)*k;
    for(int i=x;i<=r[blg[x]];++i) a[i]+=k;
    for(int i=l[blg[y]];i<=y;++i) a[i]+=k;
    for(int i=blg[x]+1;i<blg[y];++i) d[i]+=k;
}
ll query(int x,int y){ //询问区间加的操作
    ll ans=0;
    if(blg[x]==blg[y]){
        for(int i=x;i<=y;++i)
            ans+=a[i]+d[blg[i]];
        return ans;
    }
    for(int i=x;i<=r[blg[x]];++i) ans+=a[i]+d[blg[i]];
    for(int i=l[blg[y]];i<=y;++i) ans+=a[i]+d[blg[i]];
    for(int i=blg[x]+1;i<blg[y];++i) ans+=sum[i]+block*d[i];
    return ans;
}

int main(){
    n=read();q=read();
    for(int i=1;i<=n;++i)
        a[i]=read();
    build();
    while(q--){
        char op=getchar();
        while(!isupper(op)) op=getchar();
        int L=read(),R=read();
        if(op=='Q') printf("%lld\n",query(L,R));
        else update(L,R,read());
    }
    return 0;
}

于是一道模板题热热身之后,大家应该有些分块思路了吧、?


分析(2)

于是乎该怎么办呢?(这个懒标记骗的分不满意啊)那么经过深思熟虑之后,我终于发现了这道题原来是可以用分块暴力来做的。具体怎么实现呢?其实就是说我们要维护某个点的话,就是维护他所在的那块区间里的值。什么值呢? 第一个值是该节点跳出该区间所需的步数,第二个值是该节点跳出该区间后到达的下一个节点的位置(注意下一个节点不一定在该区间相邻的区间内)。于是乎这道题我就用个分块维护区间信息的方法A了此题。那么为什么用分块做效率较高呢?因为分块时我们对于区间的操作只有一个预处理(n)+分块询问、维护(msqrt(n))的时间复杂度,即:O(n+msqrt(n)),这已经算是对于此问题一个较优的解法了(当然更优的还有动态树lct)。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int M=2e5+100;
inline int read(){ //快读 
    int x=0; char c=getchar();
    while(!isdigit(c)) c=getchar();
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    return x;
}
int n,m,block,num;
int k[M],l[M],r[M],blg[M],ans[M],to[M];

inline void build(){  //建立分块区间 
    block=sqrt(n);
    num=n/block; if(n%block) ++num;
    for(int i=1;i<=num;++i)
        l[i]=(i-1)*block+1,r[i]=i*block;
    r[num]=n;
    for(int i=1;i<=n;++i)
        blg[i]=(i-1)/block+1;
}

inline void work(int x,int y){ //分块区间维护 
    for(int i=y;i>=x;--i){
        int nxt=i+k[i];
        (nxt>r[blg[i]])?
        (ans[i]=1,to[i]=nxt):
        (ans[i]=ans[nxt]+1,to[i]=to[nxt]);
    }
}

inline int query(int now){ //单点询问
    int res=ans[now],nxt=to[now];
    for(int i=blg[now]+1;nxt<=n;++i)
        res+=ans[nxt],nxt=to[nxt];
    return res;
} 

int main(){
    n=read(); build();
    for(int i=1;i<=n;++i)
        k[i]=read();
    work(1,n);  //先维护一下整个区间 
    m=read();
    while(m--){
        int op=read();
        if(op==1){
            int now=read()+1;
            printf("%d\n",query(now));
        }
        else{
            int now=read()+1,kk=read();
            k[now]=kk;
            work(l[blg[now]],r[blg[now]]); //这里只需维护单个分块区间 
        }
    }
    return 0;
}

于是乎,这道题我们就可以愉快的用分块A了。


LCT艹法

那么...如果要高级一点的话,我们是不是可以考虑一下 LCT呢?
(如果你还不懂LCT:1. 你可以跳过一下内容; 2.你可以学习一下 LCT ,温馨提示,学习LCT的一个前提就是你会 Splay ,然后树剖会不会没关系,问题貌似不大)

那么这道题为什么可以用 LCT 来做呢?
我们考虑一下,从任意一个节点弹飞后会到达的节点 有且只有 一个
而且任意节点指向的节点必然在当前节点的后面(题目中的性质)

那么我们是不是可以建一棵树来维护这些父子信息呢?当然可以!
但是如何表示绵羊被弹飞了呢?其实我们只需要加一个不存在的点设为弹飞节点就行了
任意点上的羊要是找不到后面的节点了(即被弹飞了),就连向这个不存在的点
而每次询问我们就拎出询问该到根节点(上述的弹飞节点)的这条路径,输出树的 size-1 就行了
那么...为什么输出 size-1 就可以了?
我们可以发现某节点上的绵羊被弹次数等于该节点在树中(根为弹飞节点)的 深度-1
而这条路径被我们拉出来形成一个小 splay 了之后它的 size 不就是等于深度了么?

那么一个节点的父节点要被修改了怎么办我们只需要 cut 掉老的边, link 上新的边就行了
而这些操作我们都可以用 LCT 简单实现!

//by Judge (忽然爱上压行,将就一下)
#include<iostream>
#include<cstdio>
#define ls ch[x][0]
#define rs ch[x][1]
#define min(a,b) ((a)<(b)?(a):(b))
const int M=2e5+100;
int nxt[M],f[M],ch[M][2],siz[M],stk[M],rev[M];
inline int get(int x){ return ch[f[x]][1]==x; } //splay得到父子关系
inline void pushr(int x){ std::swap(ls,rs),rev[x]^=1; }
inline void pushup(int x){ siz[x]=1+siz[ch[x][0]]+siz[ch[x][1]]; }
inline void pushdown(int x){ if(rev[x]) pushr(ls),pushr(rs),rev[x]=0; }
inline bool nroot(int x){ return ch[f[x]][0]==x||ch[f[x]][1]==x; } //判断是否非根
inline void rotate(int x){ // rotate , Splay 里面的不解释 
    int y=f[x],z=f[y],sn=get(x),b=ch[x][sn^1];
    if(nroot(y)) ch[z][get(y)]=x; ch[y][sn]=b,ch[x][sn^1]=y;
    if(b) f[b]=y; f[y]=x,f[x]=z,pushup(y);
}
inline void splay(int x){ // splay 直旋到根的模板
    int y=x,z=0; stk[++z]=y;
    while(nroot(y)) stk[++z]=y=f[y];
    while(z) pushdown(stk[z--]);
    for(y=f[x];nroot(x);rotate(x),y=f[x])
        if(nroot(y)) rotate(get(x)^get(y)?x:y);
    pushup(x);
}
inline void access(int x){ for(int y=0;x;x=f[y=x]) splay(x),rs=y,pushup(x); }  //打通 x 到根的路径
inline void find_root(int x){ access(x),splay(x); while(ls) pushdown(x),x=ls; }
inline void make_root(int x){ access(x),splay(x),pushr(x); }  //将 x 设为根
//(如果你对LCT不是特别掌握的话,一定要好好思考为什么要 pushr !以及不 pushr 所带来的后果)
inline void spilt(int x,int y){ make_root(x),access(y),splay(y); } //拉出 x - y 的路径
inline void link(int x,int y){ make_root(x),find_root(y),f[x]=y; } //连边
inline void cut(int x,int y){ make_root(x),find_root(y),f[x]=ch[y][0]=0,pushup(y); } //切边
int main(){
    int n,m,x,k,opt; scanf("%d",&n);
    for(int i=1;i<=n+1;++i) siz[i]=1; //每个节点初始独立,size = 1
    for(int u=1,v;u<=n;++u)
        scanf("%d",&k),nxt[u]=min(u+k,n+1),f[u]=nxt[u];
        //注意这里不要作死去 link !会T的! 本人亲测(好吧是坑)
    scanf("%d",&m);
    while(m--){
        scanf("%d",&opt);
        switch(opt){ //两种操作
            case 1: scanf("%d",&x),++x,spilt(n+1,x),printf("%d\n",siz[x]-1); break;
            case 2: scanf("%d%d",&x,&k),++x,cut(nxt[x],x),nxt[x]=min(x+k,n+1),link(nxt[x],x); break;
        }
    } return 0;
}

然后这道题貌似也没什么好说的了。。。那么,拜拜! _(:з」∠)_

ヾ( ̄▽ ̄)Bye~Bye~

转载于:https://www.cnblogs.com/Judge/p/9462790.html

内容概要:本文档主要展示了C语言中关于字符串处理、指针操作以及动态内存分配的相关代码示例。首先介绍了如何实现键值对(“key=value”)字符串的解析,包括去除多余空格和根据键获取对应值的功能,并提供了相应的测试用例。接着演示了从给定字符串中分离出奇偶位置字符的方法,并将结果分别存储到两个不同的缓冲区中。此外,还探讨了常量(const)修饰符在变量和指针中的应用规则,解释了不同类型指针的区别及其使用场景。最后,详细讲解了如何动态分配二维字符数组,并实现了对这类数组的排序与释放操作。 适合人群:具有C语言基础的程序员或计算机科学相关专业的学生,尤其是那些希望深入理解字符串处理、指针操作以及动态内存管理机制的学习者。 使用场景及目标:①掌握如何高效地解析键值对字符串并去除其中的空白字符;②学会编写能够正确处理奇偶索引字符的函数;③理解const修饰符的作用范围及其对程序逻辑的影响;④熟悉动态分配二维字符数组的技术,并能对其进行有效的排序和清理。 阅读建议:由于本资源涉及较多底层概念和技术细节,建议读者先复习C语言基础知识,特别是指针和内存管理部分。在学习过程中,可以尝试动手编写类似的代码片段,以便更好地理解和掌握文中所介绍的各种技巧。同时,注意观察代码注释,它们对于理解复杂逻辑非常有帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值