原题题面
题面由我校翻译组倾情翻译。
最近, SPY \text{SPY} SPY 已经退出了 XCPC \text{XCPC} XCPC(中国大学生编程竞赛)。但他零开始学习算法并赢得 ICPC \text{ICPC} ICPC(国际大学生编程竞赛)金牌的回忆仍然历历在目,所以他正在寻找一个青年作为他的学徒,取得金牌。 SPY \text{SPY} SPY 非常受欢迎,有 n n n 个青年想成为他的学徒。由于 SPY \text{SPY} SPY只需要一个学徒,所以他给这些青年设置了一个测试。
规则如下:
这些青年从 1 1 1 到 n n n 编号。 SPY \text{SPY} SPY 将按顺序面试这些青年,第 i i i 个青年将在第 i i i 个面试中接受测试。每个青年面试结束后, SPY \text{SPY} SPY 会得到他/她的智商指数( IQ \text{IQ} IQ,一个范围在 [ 0 , 202 3 202 3 2023 ] [0,2023^{2023^{2023}}] [0,202320232023] 的整数), SPY \text{SPY} SPY 可以决定是否接受他/她。一旦他接受了一个青年,测试就结束了,他不会再面试接下来的青年。一旦他拒绝了一个青年,他就不会再给他/她机会。
注意,没有两个青年具有相同的 IQ \text{IQ} IQ。 SPY \text{SPY} SPY 有一种特殊的策略来找到一个 IQ \text{IQ} IQ 高的青年。他在测试之前设置一个整数 k k k ( 0 ≤ k < n ) (0≤k<n) (0≤k<n)。
1、无论前 k k k 个青年有多聪明,他们都会被拒绝。 SPY \text{SPY} SPY 会记录前 k k k 个青年中的最高 IQ \text{IQ} IQ 数值 x x x。如果 k = 0 k=0 k=0,则 x = − 1 x=−1 x=−1。
2、然后他将面试第 k + 1 k+1 k+1 到 n − 1 n-1 n−1 个青年。一旦 SPY \text{SPY} SPY 面试到一个 IQ \text{IQ} IQ 高于 x \text{x} x 的青年,他会接受他/她并结束测试。
3、如果没有青年被接受, SPY \text{SPY} SPY 将接受第 n n n 个青年。
这些青年的 IQ \text{IQ} IQ 排名是随机的,也就是说他们的排名是 1 1 1 到 n n n 的一个排列,并且 n ! n! n! 种可能的情况发生的概率相等。尽管 SPY \text{SPY} SPY 是一个算法大师,但是他还是很难设定数字 k k k。请你帮助他设定一个最小化数字 k k k,使得他选择最大 IQ \text{IQ} IQ 青年的概率最大。
样例
输入样例:
8
1
2
3
4
9000
9001
9002
9003
输出样例:
0
0
1
1
3311
3311
3311
3312
思路解析
首先我们分析一手当有 n n n 个人的时候, k k k 对应的概率。
我们可以枚举他的位置为 i i i,对于这个位置,如果他被选中,我们显然应该让前 i − 1 i-1 i−1 个数字的最大值选前 k k k 个,这样可以保证 SPY \text{SPY} SPY 不会在他之前选到 NPY \text{NPY} NPY。
因为最大值不在前 k k k 个则会在 [ k + 1 , i − 1 ] [k+1,i-1] [k+1,i−1] 这个区间被选中,只有最大值在前 k k k 项才能避免。
由于我们选择哪一个位置概率都一样,所以因此我们得到公式:
P
=
∑
i
=
k
+
1
n
1
n
×
k
i
−
1
=
k
n
∑
i
=
k
+
1
n
1
i
−
1
=
k
n
∑
i
=
k
n
−
1
1
i
\begin{aligned} P&=\sum_{i=k+1}^n \frac 1 n \times \frac k {i-1}\\ &=\frac k n\sum_{i=k+1}^n \frac 1 {i-1}\\ &=\frac k n\sum_{i=k}^{n-1} \frac 1 {i} \end{aligned}
P=i=k+1∑nn1×i−1k=nki=k+1∑ni−11=nki=k∑n−1i1
由于前面一项不断增加,后面一项不断变小,不难发现图像长这样:
因此我们发现这个函数有单峰性质,可以三分求极值。
时间复杂度
O
(
log
3
n
)
O(\log_3n)
O(log3n)。
代码实现
由于一些精度上的问题,所以这里的三分应该是求出一个小区间,然后暴力在小区间找,时间复杂度不变。
#include<bits/stdc++.h>
#define LL long long
#define LD long double
using namespace std;
const LL N=1e4;
LL T,n;
LD sum[N+5];
LD cal(LL n,LL k)
{
if(k==0)return 1.000/n;
return 1.000*k/n*(sum[n-1]-sum[k-1]);
}
LL solve(LL n)
{
LL l=0,r=n-1;
while(l+10<r)
{
LL d=(r-l+1)/3;
LL lmid=l+d,rmid=r-d;
if(cal(n,lmid)<cal(n,rmid)) l=lmid;
else r=rmid;
}
LL ans=-1;
LD mx=-1;
for(int i=l;i<=r;i++)
{
LD t=cal(n,i);
if(mx<t)mx=t,ans=i;
}
return ans;
}
int main()
{
for(int i=1;i<=N;i++)
{
sum[i]=sum[i-1]+(1.0000/i);
}
scanf("%lld",&T);
while(T--)
{
scanf("%lld",&n);
printf("%lld\n",solve(n));
}
}