zoukankan      html  css  js  c++  java
  • P3203 [HNOI2010]弹飞绵羊 —— 懒标记?分块?LCT?...FAQ orz

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

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

    题目描述

    某天,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
    ",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
    ",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
    ",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
    ",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~

  • 相关阅读:
    ASP.NET在禁用视图状态的情况下仍然使用ViewState对象【转】
    Atcoder Regular Contest 061 D Card Game for Three(组合数学)
    Solution 「CERC 2016」「洛谷 P3684」机棚障碍
    Solution 「CF 599E」Sandy and Nuts
    Solution 「洛谷 P6021」洪水
    Solution 「ARC 058C」「AT 1975」Iroha and Haiku
    Solution 「POI 2011」「洛谷 P3527」METMeteors
    Solution 「CF 1023F」Mobile Phone Network
    Solution 「SP 6779」GSS7
    Solution 「LOCAL」大括号树
  • 原文地址:https://www.cnblogs.com/Judge/p/9462790.html
Copyright © 2011-2022 走看看