2020牛客暑期多校训练营(第六场)J.Josephus Transform(线段树二分+置换快速幂)

题目

长为n(n<=1e5)的排列,进行m(m<=1e5)次操作,保证n*m<=1e6

第i次操作,给定(ki,xi),要求将现在的排列,

按约瑟夫环ki步取数的方式,取出一个新排列,重复xi次,

第i次的操作是在第i-1次结束后的排列上进行操作的,初始排列是[1,2,..,n]

求最终排列

思路来源

https://blog.csdn.net/liufengwei1/article/details/107615435

题解

先用线段树二分求出一次约瑟夫的新排列,考虑用线段树二分实现,

设上一次取的位置为pos,则要么在[pos,n]里二分,要么在[1,pos]里二分,

线段树二分[1,x]里第一个满足前缀sumx为rank的位置,

 

置换快速幂,则考虑,第1,2,3..,n个位置,一次的映射是怎样的,

然后将乘号理解成[],因为映射(置换)也是可结合的,搞出x次之后的映射,

再对下标对应的值转移到的位置进行赋值操作,

这里写的有点搓,进行了四次赋数组值的运算

 

也有更省时的做法,首先,线段树二分不可省,

然后考虑出对每个置换循环节弄出一个环,

x次置换就是在环上往后转x%cyc个位置,其中cyc为环长,

这样就省去了快速幂的时间

代码

#include<bits/stdc++.h>
using namespace std;
#define pb push_back
const int N=1e5+10;
int n,m,k,x,now[N],nex[N],tmp[N],ans[N],to[N],pos;
struct segtree{
	int n;
	struct node{int l,r,v;}e[N<<2];
	#define l(p) e[p].l
	#define r(p) e[p].r
	#define v(p) e[p].v
	void up(int p){v(p)=v(p<<1)+v(p<<1|1);}
	void bld(int p,int l,int r){
		l(p)=l;r(p)=r;
		if(l==r){v(p)=1;return;}
		int mid=l+r>>1;
		bld(p<<1,l,mid);bld(p<<1|1,mid+1,r);
		up(p);
	}
	void init(int _n){n=_n;bld(1,1,n);}
	void chg(int p,int x,int v){
		if(l(p)==r(p)){v(p)=v;return;}
		int mid=l(p)+r(p)>>1;
		chg(p<<1|(x>mid),x,v);
		up(p);
	}
	int cnt(int p,int ql,int qr){
		if(ql<=l(p)&&r(p)<=qr)return v(p);
		int mid=l(p)+r(p)>>1,res=0;
		if(ql<=mid)res+=cnt(p<<1,ql,qr);
		if(qr>mid)res+=cnt(p<<1|1,ql,qr);
		return res;
	}
	int kth(int p,int rk){
		if(l(p)==r(p))return l(p);
		if(v(p<<1)>=rk)return kth(p<<1,rk);
        return kth(p<<1|1,rk-v(p<<1));
	}
}seg;
void ksm(int *ans,int *x,int w){//倍增x^w
    for(int i=1;i<=n;++i){
        ans[i]=i;
    }
    for(;w;w>>=1){
        if(w&1){
            for(int i=1;i<=n;++i){
                tmp[i]=ans[x[i]];//ans=ans*x
            }
            for(int i=1;i<=n;++i){
                ans[i]=tmp[i];
            }
        }
        for(int i=1;i<=n;++i){
            tmp[i]=x[x[i]];//x=x*x
        }
        for(int i=1;i<=n;++i){
            x[i]=tmp[i];
        }
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i){
        now[i]=i;
    }
    for(int i=1;i<=m;++i){
        scanf("%d%d",&k,&x);
        seg.init(n);
        int c=0;
        pos=k;
        seg.chg(1,pos,0);
        nex[++c]=now[pos];
        for(int j=n-1;j;j--){//还剩j个人没被安排 往后走nk步
            int nk=(k%j==0?j:k%j),bac=seg.cnt(1,pos,n),fro=j-bac;
            if(bac>=nk){//后面back个 从后一半找
                pos=seg.kth(1,fro+nk);
            }
            else{//后面不够 从前一半找
                pos=seg.kth(1,nk-bac);
            }
            seg.chg(1,pos,0);
            nex[++c]=now[pos];
        }
        for(int j=1;j<=n;++j){
            to[now[j]]=j;
        }
        for(int j=1;j<=n;++j){
            ans[j]=to[nex[j]];//映射序列ans
        }
        ksm(nex,ans,x);//x次映射序列nex
        for(int j=1;j<=n;++j){
            ans[j]=now[nex[j]];
        }
        for(int j=1;j<=n;++j){
            now[j]=ans[j];
        }
    }
    for(int i=1;i<=n;++i){
        printf("%d%c",now[i]," \n"[i==n]);
    }
    return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Code92007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值