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

题目

题目大意

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


正解

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

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

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

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

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


代码(未AC)

最近没有AC的题目很多,代码都摆在这里,以后也不一定会去调试了……
话说程序里我用的是小数来逼近分数的方法,不过为了追求常数,我把小数变成了分数的形式。当然,这个分数的分母都是 2 2 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",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",n,0);
	else if (num==one+1)
		printf("%d %d\n",0,n);
	else if (num==2*one+2)
		printf("%d %d\n",-n,0);
	else if (num==3*one+3)
		printf("%d %d\n",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\n",(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\n",x,y);	
		else if (num<2*one+2)
			printf("%d %d\n",-y,x);
		else if (num<3*one+3)
			printf("%d %d\n",-x,-y);
		else
			printf("%d %d\n",y,-x);
	}
	return 0;
}

总结

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值