loj 数列分块入门 1 ~ 9 做题笔记
前言
做了 hzwer 的分块系列(
%%% hzwer 学长
loj6277 数列分块入门 1
给定数列,区间修改,单点查询。
区间修改可以打永久标记。没什么好说的,毕竟是 1。
#include <bits/stdc++.h> #define int long long using namespace std; inline int read(){ int x=0,f=0;char ch=getchar(); while(!isdigit(ch))f^=!(ch^45),ch=getchar(); while(isdigit(ch))x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return f?-x:x; } inline void write(int x){ if(x<0)x=-x,putchar('-'); if(x>=10)write(x/10); putchar(x%10+'0'); } inline void writeln(int x){write(x);puts("");} int n; int a[50005]; int block,num; int L[500005],R[500005]; int pos[500005]; int sum[500005],add[500005]; 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<=num;i++){ for(int j=L[i];j<=R[i];j++){ pos[j]=i; sum[i]+=a[j]; } } } void update(int l,int r,int k){ int x=pos[l],y=pos[r]; if(x==y){ for(int i=l;i<=r;i++){ a[i]+=k; } sum[x]+=k*(r-l+1); }else{ for(int i=x+1;i<=y-1;i++){ add[i]+=k; } for(int i=l;i<=R[x];i++){ a[i]+=k; sum[x]+=k; } for(int i=L[y];i<=r;i++){ a[i]+=k; sum[y]+=k; } } } int query(int l,int r){ int x=pos[l],y=pos[r]; int ans=0; if(x==y){ for(int i=l;i<=r;i++)ans+=a[i]; ans+=add[x]*(r-l+1); } else{ for(int i=x+1;i<=y-1;i++){ ans+=sum[i]+add[i]*(R[i]-L[i]+1); } for(int i=l;i<=R[x];i++){ ans+=a[i]; } ans+=add[x]*(R[x]-l+1); for(int i=L[y];i<=r;i++){ ans+=a[i]; } ans+=add[y]*(r-L[y]+1); } return ans; } signed main(){ n=read(); for(int i=1;i<=n;i++)a[i]=read(); build(); while(n--){ int op=read(),l=read(),r=read(),c=read(); if(op==0)update(l,r,c); else writeln(query(r,r)); } #ifndef ONLINE_JUDGE system("pause"); #endif return 0; }
loj6278 数列分块入门 2
给定数列,区间加,区间查询小于 的个数。
233333333 线段树你做不了了 hhh
区间小于某个数可以二分求解。复制一份辅助数组 ,初值全部等于 ,对每个块内进行排序。
#include <bits/stdc++.h> #define int long long using namespace std; inline int read(){ int x=0,f=0;char ch=getchar(); while(!isdigit(ch))f^=!(ch^45),ch=getchar(); while(isdigit(ch))x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return f?-x:x; } inline void write(int x){ if(x<0)x=-x,putchar('-'); if(x>=10)write(x/10); putchar(x%10+'0'); } inline void writeln(int x){write(x);puts("");} int n; int block,num; int pos[50005]; int sum[50005]; int add[50005]; int L[50005],R[50005]; int a[50005],d[50005]; 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++)pos[i]=(i-1)/block+1; for(int i=1;i<=num;i++){ for(int j=L[i];j<=R[i];j++)sum[i]+=a[j]; sort(d+L[i],d+R[i]+1); } } void update(int l,int r,int k){ int x=pos[l],y=pos[r]; if(x==y){ for(int i=l;i<=r;i++)a[i]+=k; sum[x]+=(r-l+1)*k; for(int i=L[x];i<=R[x];i++)d[i]=a[i]; sort(d+L[x],d+R[x]+1); }else{ for(int i=x+1;i<=y-1;i++)add[i]+=k; //------------------ for(int i=l;i<=R[x];i++)a[i]+=k; sum[x]+=(R[x]-l+1)*k; for(int i=L[x];i<=R[x];i++)d[i]=a[i]; sort(d+L[x],d+R[x]+1); //------------------ for(int i=L[y];i<=r;i++)a[i]+=k; sum[y]+=(r-L[y]+1)*k; for(int i=L[y];i<=R[y];i++)d[i]=a[i]; sort(d+L[y],d+R[y]+1); } } int query(int l,int r,int k){ int ans=0; int x=pos[l],y=pos[r]; if(x==y){ for(int i=l;i<=r;i++)if(a[i]+add[x]<k)ans++; }else{ for(int i=l;i<=R[x];i++)if(a[i]+add[x]<k)ans++; for(int i=L[y];i<=r;i++)if(a[i]+add[y]<k)ans++; for(int i=x+1;i<=y-1;i++){ ans+=lower_bound(d+L[i],d+R[i]+1,k-add[i])-d-L[i]; } } return ans; } signed main(){ n=read(); for(int i=1;i<=n;i++)a[i]=d[i]=read(); build(); while(n--){ int op=read(),l=read(),r=read(),c=read(); if(op==0)update(l,r,c); else writeln(query(l,r,c*c)); } #ifndef ONLINE_JUDGE system("pause"); #endif return 0; }
loj6279 数列分块入门 3
给定数列,区间加,区间前驱。
loj 的分块题就离不开区间加法
前驱可以二分求解。
#include <bits/stdc++.h> #define endl '\n' using namespace std; const int N = 1e5+10; int n; int a[N]; int L[N],R[N],pos[N],add[N],d[N]; int block,num; 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++){ pos[i]=(i-1)/block+1; } for(int i=1;i<=num;i++){ sort(d+L[i],d+R[i]+1); } } void update(int l,int r,int k){ int x=pos[l],y=pos[r]; if(x==y){ for(int i=l;i<=r;i++)a[i]+=k; for(int i=L[x];i<=R[x];i++)d[i]=a[i]; sort(d+L[x],d+R[x]+1); return; } //左端零散块 for(int i=l;i<=R[x];i++)a[i]+=k; for(int i=L[x];i<=R[x];i++)d[i]=a[i]; sort(d+L[x],d+R[x]+1); //中间完整块 for(int i=x+1;i<=y-1;i++)add[i]+=k; //右端零散块 for(int i=L[y];i<=r;i++)a[i]+=k; for(int i=L[y];i<=R[y];i++)d[i]=a[i]; sort(d+L[y],d+R[y]+1); } int query(int l,int r,int k){ int x=pos[l],y=pos[r]; if(x==y){ int ans=-0x3f3f3f3f; for(int i=l;i<=r;i++){ if(a[i]+add[x]<k){ ans=max(ans,a[i]+add[x]); } } return ans==-0x3f3f3f3f?-1:ans; } int ans=-0x3f3f3f3f; for(int i=l;i<=R[x];i++){ if(a[i]+add[x]<k){ ans=max(ans,a[i]+add[x]); } } for(int i=L[y];i<=r;i++){ if(a[i]+add[y]<k){ ans=max(ans,a[i]+add[y]); } } //完整块 for(int i=x+1;i<=y-1;i++){ int cur=lower_bound(d+L[i],d+R[i]+1,k-add[i])-d; if(cur==L[i])continue; ans=max(ans,d[cur-1]+add[i]); } return ans==-0x3f3f3f3f?-1:ans; } int main(){ ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr); cin>>n; for(int i=1;i<=n;i++)cin>>a[i],d[i]=a[i]; build(); while(n--){ int opt,l,r,c; cin>>opt>>l>>r>>c; if(opt==0){ update(l,r,c); }else{ cout<<query(l,r,c)<<endl; } } return 0; }
loj6280 数列分块入门 4
数列区间加区间求和。
非常正常的一道线段树分块题。
#include <bits/stdc++.h> #define int long long using namespace std; inline int read(){ int x=0,f=0;char ch=getchar(); while(!isdigit(ch))f^=!(ch^45),ch=getchar(); while(isdigit(ch))x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return f?-x:x; } inline void write(int x){ if(x<0)x=-x,putchar('-'); if(x>=10)write(x/10); putchar(x%10+'0'); } inline void writeln(int x){write(x);puts("");} int n; int a[50005]; int block,num; int L[500005],R[500005]; int pos[500005]; int sum[500005],add[500005]; 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<=num;i++){ for(int j=L[i];j<=R[i];j++){ pos[j]=i; sum[i]+=a[j]; } } } void update(int l,int r,int k){ int x=pos[l],y=pos[r]; if(x==y){ for(int i=l;i<=r;i++){ a[i]+=k; } sum[x]+=k*(r-l+1); }else{ for(int i=x+1;i<=y-1;i++){ add[i]+=k; } for(int i=l;i<=R[x];i++){ a[i]+=k; sum[x]+=k; } for(int i=L[y];i<=r;i++){ a[i]+=k; sum[y]+=k; } } } int query(int l,int r){ int x=pos[l],y=pos[r]; int ans=0; if(x==y){ for(int i=l;i<=r;i++)ans+=a[i]; ans+=add[x]*(r-l+1); } else{ for(int i=x+1;i<=y-1;i++){ ans+=sum[i]+add[i]*(R[i]-L[i]+1); } for(int i=l;i<=R[x];i++){ ans+=a[i]; } ans+=add[x]*(R[x]-l+1); for(int i=L[y];i<=r;i++){ ans+=a[i]; } ans+=add[y]*(r-L[y]+1); } return ans; } signed main(){ n=read(); for(int i=1;i<=n;i++)a[i]=read(); build(); while(n--){ int op=read(),l=read(),r=read(),c=read(); if(op==0)update(l,r,c); else writeln(query(l,r)%(c+1)); } #ifndef ONLINE_JUDGE system("pause"); #endif return 0; }
loj6281 数列分块入门 5
给定数列,区间开方,区间求和。
,在这个范围内, 最多开根 次就会变为 ,此时再开根是无意义的,所以维护 表示第 块是否都变为 或 (反正再开根无意义的数),若 暴力操作整块。
#include <bits/stdc++.h> #define endl '\n' using namespace std; const int N = 5e4 + 10; int n; int a[N]; int pos[N],L[N],R[N],sum[N],tag[N]; int block,num; 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++){ pos[i]=(i-1)/block+1; sum[pos[i]]+=a[i]; } } void Sqrt(int x){ if(tag[x])return; sum[x]=0;tag[x]=1; for(int i=L[x];i<=R[x];i++){ a[i]=sqrt(a[i]); sum[x]+=a[i]; if(a[i]>1)tag[x]=0; } } void update(int l,int r){ int x=pos[l],y=pos[r]; if(x==y){ for(int i=l;i<=r;i++){ sum[x]-=a[i]; a[i]=sqrt(a[i]); sum[x]+=a[i]; } return; } for(int i=l;i<=R[x];i++){ sum[x]-=a[i]; a[i]=sqrt(a[i]); sum[x]+=a[i]; } for(int i=x+1;i<=y-1;i++)Sqrt(i); for(int i=L[y];i<=r;i++){ sum[y]-=a[i]; a[i]=sqrt(a[i]); sum[y]+=a[i]; } } int query(int l,int r){ int x=pos[l],y=pos[r]; if(x==y){ int ans=0; for(int i=l;i<=r;i++)ans+=a[i]; return ans; } int ans=0; for(int i=l;i<=R[x];i++)ans+=a[i]; for(int i=x+1;i<=y-1;i++)ans+=sum[i]; for(int i=L[y];i<=r;i++)ans+=a[i]; return ans; } int main(){ ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr); cin>>n; for(int i=1;i<=n;i++)cin>>a[i]; build(); while(n--){ int opt,l,r,c; cin>>opt>>l>>r>>c; if(opt==0){ update(l,r); }else{ cout<<query(l,r)<<endl; } } return 0; }
loj6282 数列分块入门 6
数列单点插入单点查询。
恕本人蒟蒻,实在想不出这题如何用分块解答。
看到插入果断选择 splay。
#include <bits/stdc++.h> #define endl '\n' using namespace std; const int N = 2e5+10; int n,root; struct node{ int son[2],fa,val,siz; }tr[N]; #define ls(x) (tr[x].son[0]) #define rs(x) (tr[x].son[1]) #define fa(x) (tr[x].fa) inline void pushup(int x){tr[x].siz=tr[ls(x)].siz+tr[rs(x)].siz+1;} void rotate(int x){ int y=fa(x),z=fa(y); int k=rs(y)==x; tr[z].son[rs(z)==y]=x,fa(x)=z; tr[y].son[k]=tr[x].son[k^1],fa(tr[x].son[k^1])=y; tr[x].son[k^1]=y,fa(y)=x; pushup(y);pushup(x); } void splay(int x,int goal){ while(fa(x)!=goal){ int y=fa(x),z=fa(y); if(z!=goal){ if((rs(z)==y) ^ (rs(y)==x))rotate(x); else rotate(y); } rotate(x); } if(!goal)root=x; } int kth(int k){ int x=root; while(true){ if(tr[ls(x)].siz>=k)x=ls(x); else if(tr[ls(x)].siz+1>=k)return x; else k-=tr[ls(x)].siz+1,x=rs(x); } } int a[N]; int idx; int New(int val,int fa){ tr[++idx].fa=fa;tr[idx].siz=1;tr[idx].val=val;return idx; } void build(int &p,int l,int r,int fa){ if(l>r)return; int mid=l+r>>1; p=New(a[mid],fa); build(ls(p),l,mid-1,p); build(rs(p),mid+1,r,p); pushup(p); } void init(){ root=New(-0x3f3f3f3f,0); rs(root)=New(0x3f3f3f3f,root); tr[root].siz=2; build(ls(rs(root)),1,n,rs(root)); pushup(rs(root));pushup(root); } void insert(int pos,int val){ int x=kth(pos-1),y=kth(pos); splay(x,0);splay(y,x); ls(y)=New(val,y); pushup(y);pushup(x); } int main(){ ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr); cin>>n; for(int i=1;i<=n;i++)cin>>a[i]; init(); while(n--){ int p,l,r,c; cin>>p>>l>>r>>c; if(p==0){ ++l; insert(l,r); }else{ ++r; splay(kth(r),0); cout<<tr[root].val<<endl; } } return 0; }
loj6283 数列分块入门 7
给定数列,区间乘,区间加,单点查询。
维护两个懒标记,但是这里情况比较特殊,并不是使用永久标记,而是会类似线段树那样下传标记,注意先乘后加,然后正常维护即可。
#include <bits/stdc++.h> #define endl '\n' #define int long long #define mod 10007 using namespace std; const int N = 1e5+10; int n; int a[N]; int pos[N],L[N],R[N],add[N],mul[N]; int block,num; void build(){ block=sqrt(n); num=n/block; if(n%block)num++; for(int i=1;i<=num;i++){ mul[i]=1; L[i]=(i-1)*block+1; R[i]=i*block; } R[num]=n; for(int i=1;i<=n;i++){ pos[i]=(i-1)/block+1; } } void clear(int x){ for(int i=L[x];i<=R[x];i++){ a[i]=a[i]*mul[x]%mod; a[i]=(a[i]+add[x])%mod; } mul[x]=1;add[x]=0; } void Mul(int l,int r,int k){ int x=pos[l],y=pos[r]; if(x==y){ clear(x); for(int i=l;i<=r;i++){ a[i]=a[i]*k%mod; } return; } clear(x); for(int i=l;i<=R[x];i++){ a[i]=a[i]*k%mod; } clear(y); for(int i=L[y];i<=r;i++){ a[i]=a[i]*k%mod; } for(int i=x+1;i<y;i++){ add[i]=add[i]*k%mod;mul[i]=mul[i]*k%mod; } } void Add(int l,int r,int k){ int x=pos[l],y=pos[r]; if(x==y){ clear(x); for(int i=l;i<=r;i++){ a[i]=(a[i]+k)%mod; } return; } clear(x); for(int i=l;i<=R[x];i++){ a[i]=(a[i]+k)%mod; } for(int i=x+1;i<y;i++){ add[i]=(add[i]+k)%mod; } clear(y); for(int i=L[y];i<=r;i++){ a[i]=(a[i]+k)%mod; } } signed main(){ ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr); cin>>n; for(int i=1;i<=n;i++)cin>>a[i]; build(); while(n--){ int opt,l,r,c; cin>>opt>>l>>r>>c; if(opt==0)Add(l,r,c); if(opt==1)Mul(l,r,c); if(opt==2)cout<<(a[r]*mul[pos[r]]%mod+add[pos[r]])%mod<<endl; } return 0; }
loj6284 数列分块入门 8
给定数列,求区间 个数并覆盖为 。
同样采用下放标记的方式。
在查询,如果发现第 已经覆盖成 ,就把整块加上去,如果没有打过标记就暴力统计。
#include <bits/stdc++.h> #define endl '\n' #define inf 0x3f3f3f3f using namespace std; const int N = 1e5 + 10; int a[N],n; int L[N],R[N],pos[N]; int cov[N]; int block,num; 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; cov[i]=inf; } R[num]=n; for(int i=1;i<=n;i++){ pos[i]=(i-1)/block+1; } } void clear(int x){ if(cov[x]==inf)return; for(int i=L[x];i<=R[x];i++){ a[i]=cov[x]; } cov[x]=inf; } int query(int l,int r,int c){ int x=pos[l],y=pos[r]; int ans=0; if(x==y){ clear(x); for(int i=l;i<=r;i++){ ans+=a[i]==c;a[i]=c; } return ans; } clear(x); for(int i=l;i<=R[x];i++){ ans+=a[i]==c;a[i]=c; } clear(y); for(int i=L[y];i<=r;i++){ ans+=a[i]==c;a[i]=c; } for(int i=x+1;i<y;i++){ if(cov[i]==c)ans+=R[i]-L[i]+1; if(cov[i]==inf){ for(int j=L[i];j<=R[i];j++){ ans+=a[j]==c; } } cov[i]=c; } return ans; } int main(){ ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr); cin>>n; for(int i=1;i<=n;i++)cin>>a[i]; build(); while(n--){ int l,r,c; cin>>l>>r>>c; cout<<query(l,r,c)<<endl; } return 0; }
loj6285 数列分块入门 9*
Boss 来了。
给定数列,求区间最小众数。
回忆一下我们平时是怎么算众数的,我们开桶 记录每个数出现的次数,最后扫一遍 即可。
分块预处理
看一下题目的数据范围:(也就是 int
的范围),这么大,用数组会炸掉,而如果用 std::map
,会给复杂度再加上 ,显然不优(更多详细原因阅读脚注)[1],所以需要离散化,这里使用 unique
+ lower_bound
的离散化方法。
这样得到的 数组是原 数组中对应的数的下标,输出时须留意。
离散化过后,我们就可以预处理出前缀和数组 表示前 块中 出现的次数。
对于两端零散的块,我们暴力统计;对于中间完整的块,就需要预处理了。
我们预处理出 表示第 块到第 块的众数。
区间查询众数
看一张图来理解:
此时数列已经分成 块,已用不同颜色标出。
我们查找 ,显然我们早已求出 ,现在就是要处理两端绿色部分(零散部分),两端的部分出现次数怎么算呢?先用一个桶统计,然后还得加上中间块(橙色部分)的出现次数,用 前缀和可以得到。
一点优化
注:此处设 为第 个数属于第几块。
可以发现,当 时,对于中间块的操作,使用 数组查询跟我们暴力处理没有区别, 数组统计中间块的出现次数,因为只有 块,所以优化效果微乎其微。
所以当 我们就打暴力,否则就分块处理。
说句闲话
表示每块起始的数组名从 改成了 ,因为觉得按 Shift 麻烦。
#include <bits/stdc++.h> #define endl '\n' using namespace std; const int N = 1e5 + 10; int n; int a[N],b[N]; int st[N],ed[N],pos[N]; int f[318][318]; int s[318][N]; int block,num; int m[N]; int main(){ ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr); cin>>n; for(int i=1;i<=n;i++)cin>>b[i],a[i]=b[i]; sort(b+1,b+n+1); int len=unique(b+1,b+n+1)-b-1; for(int i=1;i<=n;i++){ a[i]=lower_bound(b+1,b+len+1,a[i])-b; } block=sqrt(n);num=n/block;if(n%block)num++; for(int i=1;i<=num;i++){ st[i]=(i-1)*block+1; ed[i]=i*block; } for(int i=1;i<=n;i++)pos[i]=(i-1)/block+1; for(int i=1;i<=num;i++){ for(int j=st[i];j<=ed[i];j++)s[i][a[j]]++; for(int j=1;j<=len;j++)s[i][j]+=s[i-1][j]; } for(int i=1;i<=num;i++){ for(int j=i;j<=num;j++){ int mx=f[i][j-1]; for(int k=st[j];k<=ed[j];k++){ if(s[j][a[k]]-s[i-1][a[k]]>=s[j][mx]-s[i-1][mx]){ if(s[j][a[k]]-s[i-1][a[k]]==s[j][mx]-s[i-1][mx]){ mx=min(mx,a[k]); } else mx=a[k]; } } f[i][j]=mx; } } while(n--){ int l,r;cin>>l>>r; int x=pos[l],y=pos[r]; int ans=0; if(y-x<=1){ for(int i=l;i<=r;i++){ m[a[i]]++; if(m[a[i]]>=m[ans]){ if(m[a[i]]==m[ans])ans=min(ans,a[i]); else ans=a[i]; } } for(int i=l;i<=r;i++)m[a[i]]=0; }else{ ans=f[x+1][y-1]; for(int i=l;i<=ed[x];i++)m[a[i]]++; for(int i=st[y];i<=r;i++)m[a[i]]++; for(int i=l;i<=ed[x];i++){ if(m[a[i]]+s[y-1][a[i]]-s[x][a[i]]>=m[ans]+s[y-1][ans]-s[x][ans]){ if(m[a[i]]+s[y-1][a[i]]-s[x][a[i]]==m[ans]+s[y-1][ans]-s[x][ans])ans=min(ans,a[i]); else ans=a[i]; } } for(int i=st[y];i<=r;i++){ if(m[a[i]]+s[y-1][a[i]]-s[x][a[i]]>=m[ans]+s[y-1][ans]-s[x][ans]){ if(m[a[i]]+s[y-1][a[i]]-s[x][a[i]]==m[ans]+s[y-1][ans]-s[x][ans])ans=min(ans,a[i]); else ans=a[i]; } } for(int i=l;i<=ed[x];i++)m[a[i]]=0; for(int i=st[y];i<=r;i++)m[a[i]]=0; } cout<<b[ans]<<endl; } return 0; }
其实用
std::map
当桶处理大数据也可以,带个 其实也不会超时,麻烦就麻烦在数组 的处理上,因为按照值域来更新,不离散化值域过大,这里才是超时的原因,所以std::map
寄了。 ↩︎
本文作者:tmjyh09
本文链接:https://www.cnblogs.com/tmjyh09/p/16192081.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步