• 6383. 【NOIP2019模拟2019.10.07】果实摘取


    题目

    题目大意

    给你一个由整点组成的矩形,坐标绝对值范围小于等于(n),你在((0,0)),一开始面向((1,0)),每次转到后面第(k)个你能看到的点,然后将这条线上的点全部标记删除。
    问最后一个被标记删除的点的坐标。


    正解

    先吐槽一句,原来删除的点是一条线上的,而不是一个点……
    害得我以为是一道神题……更可恨的是,我看不出我的暴力有什么错!

    既然一次删除的点是在一条线上的,那不妨将整条线上的东西看成一个点。
    那就变成了一个约瑟夫问题(也就是猴子选大王)。
    共有(8sum_{i=1}^{n}phi(i))个(原图分成(8)个三角形,减去对角线被算(4)次,加上垂直和水平方向(4)个)
    设现在有(n)个,则将第(k)个删除之后,就变成了(n-1)个的问题。所以可以通过递归来求。设(f_i)表示(i)个人在搞完之后最后一个留下的人是谁(编号从(0)开始)
    显然(f_i=(f_{i-1}+k)mod i)

    然而这个东西似乎会爆炸,因为总数是很多的。
    考虑如何快速计算这玩意儿。现在,最主要的瓶颈就是取模操作。
    由于不一定每次都会大于模数,所以考虑加几次(k)之后取一次模。
    假设现在有(n)个,设至少增加(x)个之后要取一次模。
    于是就有了不等式:(f_n+kxgeq n+x),解得(xgeq frac{n-f_n}{k-1})
    (当然不要忘了上取整)
    这样算会快很多,但是时间看起来似乎不是很好算。
    计算一下时间:当(nleq k)时,每次(n)只能变成(n+1),这一部分时间为(O(k)).
    (n>k)时,每次相当于加上(frac{n}{k})左右。
    尽管看起来并不是很靠谱,但实际上,某个(f_n+k)之后超过了(n+1),于是取模,(f_{n+1})不会超过(k)。所以当(n)远远大于(k)时,每次(f_n)(n)相差比较大,它们的差就可以近似地认为是(n)。(或者说约等于于加上(frac{n-k}{k}),有了个(-1)的常数,就省去了)
    加上(frac{n}{k})相当于乘(frac{k+1}{k})
    于是这一部分时间大概为(O(log_frac{k+1}{k}n))
    是对数级别时间复杂度,似乎很快的样子。如果用个换底公式,把(frac{1}{lgfrac{k+1}{k}})的常数给省掉,那么看起来是(O(lg n))的时间复杂度呢,似乎很优秀。但是这个常数实际上不能省!用计算器计算一下,这个常数大概为(230260)(这里的(lg)是指(log_{10}),不是(log_2),计算器里面没有直接提供(log_2)这种东西,所以干脆直接用(log_{10}了))。
    不过还好,时间还是过得去……

    接下来变成另一个问题:寻找排名第几项的位置。
    首先,找到这个位置在哪个象限。接下来只讨论第一象限的,其它的在此基础上旋转一下就好了。
    很容易想到二分。假设我们二分出了一个分数(frac{b}{a})
    现在我们要求再斜率为(frac{b}{a})这条直线一下的点数。
    具体来说,就是这个式子:(sum_{i=1}^{n}sum_{j=1}^{n}[gcd(i,j)=1]*[frac{j}{i}<frac{b}{a}])
    这个式子可以反演(说实在的,我对反演非常不熟悉)
    有个比较重要的性质:(sum_{d|n}mu(d)=[n=1])

    [sum_{i=1}^{n}sum_{j=1}^{n}sum_{d|gcd(i,j)}mu(d)*[frac{j}{i}<frac{b}{a}] \ =sum_{i=1}^nsum_{d|i}mu(d)sum_{d|j,jleq n}[j<frac{bi}{a}] \ =sum_{i=1}^nsum_{d|i}mu(d)min(lfloorfrac{n}{d} floor,lfloorfrac{bi}{ad} floor) \ =sum_{d=1}^nmu(d)sum_{i=1}^{lfloorfrac{n}{d} floor}min(lfloorfrac{n}{d} floor,lfloorfrac{bi}{a} floor)]

    有了这条式子,就可以在(O(nln n))的时间内判断了。
    至于如何二分,题解有种比较容易理解的暴力用小数来逼近分数的方法,Cold_Chair大爷有个(Stern-Brocot Tree)上二分的强大做法(由于节点的深度可能比较深,但拐角处是(lg)级别的,所以还要二分一下在某个方向上走多长距离。而且求答案的时候,还用到了整除分块)


    代码(未AC)

    最近没有AC的题目很多,代码都摆在这里,以后也不一定会去调试了……
    话说程序里我用的是小数来逼近分数的方法,不过为了追求常数,我把小数变成了分数的形式。当然,这个分数的分母都是(2)的幂。

    using namespace std;
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <cmath>
    #include <climits>
    #define N 100000
    #define ll long long
    int n,K;
    int p[N+10],np;
    bool inp[N+10];
    int phi[N+10],mu[N+10];
    ll calc(ll tar){
    //	return (K+calc(n-1))%n;
    	ll n=1,fn=0;
    	do{
    		ll x=(n-fn -1)/(K-1) +1;
    		if (tar<n+x)
    			return fn+K*(tar-n);
    		n=n+x;
    		fn=(fn+K*x)%n;
    	}
    	while (1);
    }
    ll below(ll a,ll b){
    	ll res=0;
    	for (int d=1;d<=n;++d){
    		ll s=0,n_d=n/d;
    		for (int i=1;i*d<=n;++i)
    			s+=min(b*i/a,n_d);
    		res+=s*mu[d];
    	}
    	return res;
    }
    int main(){
    //	freopen("in.txt","r",stdin);
    	freopen("garden.in","r",stdin);
    	freopen("garden.out","w",stdout);
    	scanf("%d%d",&n,&K);
    	if (K==1){
    		printf("%d %d
    ",n,-1);
    		return 0;
    	}
    	phi[1]=1,mu[1]=1;
    	for (int i=2;i<=n;++i){
    		if (!inp[i]){
    			p[++np]=i;
    			phi[i]=i-1;
    			mu[i]=-1;
    		}
    		for (int j=1;j<=np && i*p[j]<=n;++j){
    			inp[i*p[j]]=1;
    			if (i%p[j]==0){
    				phi[i*p[j]]=phi[i]*p[j];
    				break;
    			}
    			phi[i*p[j]]=phi[i]*(p[j]-1);
    			mu[i*p[j]]=mu[i]*mu[p[j]];
    		}
    	}
    	ll one=1,all=0;
    	for (int i=2;i<=n;++i)
    		one+=phi[i]*2;
    	all=one*4+4;
    	ll num=calc(all);
    	if (num==0)
    		printf("%d %d
    ",n,0);
    	else if (num==one+1)
    		printf("%d %d
    ",0,n);
    	else if (num==2*one+2)
    		printf("%d %d
    ",-n,0);
    	else if (num==3*one+3)
    		printf("%d %d
    ",0,-n);
    	else{
    		int rank=num%(one+1);
    		ll l=0,r=n,d=0;
    		while (d<30){
    			ll mid=l+r;
    			if (below(1<<d+1,mid)<rank)
    				l=mid,r<<=1;
    			else
    				r=mid,l<<=1;
    			d++;
    		}
    //		printf("%lf
    ",(double)r/(1ll<<d));
    		int x,y;
    		long double v=(long double)r/(1ll<<d),tmp=2e9;
    		for (ll i=n;i>=1;--i){
    			ll j=r*i/(1ll<<d)/*(ll)(v*i)*/;
    			if (v-double(j)/i<tmp){
    				tmp=v-double(j)/i;
    				y=j;
    				x=i;
    			}
    		}
    		if (num<one+1)
    			printf("%d %d
    ",x,y);	
    		else if (num<2*one+2)
    			printf("%d %d
    ",-y,x);
    		else if (num<3*one+3)
    			printf("%d %d
    ",-x,-y);
    		else
    			printf("%d %d
    ",y,-x);
    	}
    	return 0;
    }
    

    总结

    审题是关键。
    反演及其不熟练,需要找时间来提升。(实际上我似乎没有AC过一道反演的题目)

  • 相关阅读:
    codevs1127
    codevs1041
    C#预处理指令
    C#基本语句与C++区别
    iOS.TextKit.01.凸版印刷效果
    iOS.常用设计模式.02.委托模式
    iOS.常用设计模式.01.单例模式
    iOS.iPad.03.UIModal
    iOS.iPad.02.UIPopoverViewController
    iOS.iPad.01.UISplitViewController
  • 原文地址:https://www.cnblogs.com/jz-597/p/11720582.html
Copyright © 2020-2023  润新知