题目
题目大意
给你一个由整点组成的矩形,坐标绝对值范围小于等于
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)
8∑i=1nϕ(i)个(原图分成
8
8
8个三角形,减去对角线被算
4
4
4次,加上垂直和水平方向
4
4
4个)
设现在有
n
n
n个,则将第
k
k
k个删除之后,就变成了
n
−
1
n-1
n−1个的问题。所以可以通过递归来求。设
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=(fi−1+k)modi
然而这个东西似乎会爆炸,因为总数是很多的。
考虑如何快速计算这玩意儿。现在,最主要的瓶颈就是取模操作。
由于不一定每次都会大于模数,所以考虑加几次
k
k
k之后取一次模。
假设现在有
n
n
n个,设至少增加
x
x
x个之后要取一次模。
于是就有了不等式:
f
n
+
k
x
≥
n
+
x
f_n+kx\geq n+x
fn+kx≥n+x,解得
x
≥
n
−
f
n
k
−
1
x\geq \frac{n-f_n}{k-1}
x≥k−1n−fn
(当然不要忘了上取整)
这样算会快很多,但是时间看起来似乎不是很好算。
计算一下时间:当
n
≤
k
n\leq k
n≤k时,每次
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}
kn−k,有了个
−
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=1n∑j=1n[gcd(i,j)=1]∗[ij<ab]
这个式子可以反演(说实在的,我对反演非常不熟悉)
有个比较重要的性质:
∑
d
∣
n
μ
(
d
)
=
[
n
=
1
]
\sum_{d|n}\mu(d)=[n=1]
∑d∣nμ(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=1∑nj=1∑nd∣gcd(i,j)∑μ(d)∗[ij<ab]=i=1∑nd∣i∑μ(d)d∣j,j≤n∑[j<abi]=i=1∑nd∣i∑μ(d)min(⌊dn⌋,⌊adbi⌋)=d=1∑nμ(d)i=1∑⌊dn⌋min(⌊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
Stern−BrocotTree上二分的强大做法(由于节点的深度可能比较深,但拐角处是
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过一道反演的题目)