[ABC200-E]Patisserie ABC 2
壹、题目大意 ¶
你有 \(n^3\) 个三元组 \((i,j,k)\),这些三元组各不相同,且 \(i,j,k\) 都是 \([1,n]\) 之间的正整数。
你现在以 \(i+j+k\) 为第一关键字,\(i\) 为第二关键字,\(j\) 为第三关键字将所有的三元组从小到大排序。
请问第 \(K\) 个三元组,它的 \(i,j,k\) 分别是多少。
贰、题解 ¶
不难发现,我们只需要快速地找到 \(i+j+k\) 的和为多少,然后枚举 \(i\),就可以直接算出 \(j,k\) 了。
所以,这道题地难点,或者说关键是找到 \(s=i+j+k\),对于找到 \(s\),这里有许多做法
§ 朴素算法 \(\mathcal O(n)\) §
我们可以通过背包求出和为每个数的三元组的个数。
然后枚举第 \(K\) 个三元组的和是多少,这个枚举过程也可以先将背包求前缀和,再用二分实现,但由于要求前缀和,时间复杂度并不会变得优秀。
之后再通过相似的枚举得到 \(i\) 的值与 \(j\) 的值,就可以求出 \(k\) 了。
时间复杂度 \(O\left(n\right)\).
§ 优化壹:没有 DP!§
我们能否使用数学方法将 \(s\) 找到?比较快速地?比如说 \(\mathcal O(1)\)?
考虑生成函数:令 \(f(x)=\sum_{i=0}^\infty x^i\),定义 \(g(x)\) 表示方案数的生成函数,根据定义有 \([x^j]g(x)\) 为三个数的和为 \(j\) 的方案数。
考虑使用 \(f(x)\) 表示出 \(g(x)\),显然有
这是为什么?先考虑一个数,首先有最基础的无限级数求和 \(f(x)\),但是一个数不能超过 \(n\),所以减去 \(x^{n+1}f(x)\),并且还有常数项 \(-1\),有三个数,所以对于 \((f(x)-x^{n+1}f(x)-1)\) 的三次方,最后就得到了 \(g(x)\).
考虑一个很经典的收敛,\(f(x)=\frac{1}{1-x}\),我们不难得到 \(g(x)\) 的闭形式:
考察 \(g(x)\) 在 \(x=0\) 处的泰勒展开:
发现对于 \(x^3,x^4,x^5\) 的系数是 \(\{1,3,6,10,15,21,28,...\}\) 是等差数列求和公式 \(S(n)={n(n+1)\over 2}\) 的数列,那么
其中,\(n\) 是一个常数。
这样,我们就可以摆脱使用 \(dp\) 推出方案数,而是可以直接算了。
对于为什么 \(g(x)\) 有上面的式子,我们可以使用容斥对这个结论进行说明:
如果我们要求 \(i+j+k=sum\) 的情况, 其中满足 \(1\le i,j,k\le n\).
首先,我们先把 \(i,j,k\le n\) 的限制扔掉,那么就有 \({sum-1\choose 2}\) 种方案(插板法),然后减去有 \(1/2/3\) 个数字超过 \(n\) 的情况:
\(3\) 个数都大于 \(n\) 的情况,我们可以强制给每个数都塞 \(n\),剩下的 \(sum-3n\) 也使用插板法解决,有 \(sum-3n-1\choose 2\) 种方案。
然后,我们钦定一个数超过 \(n\),显然有三个这样的数字,所以方案数减去 \(sum-2n-1\choose 2\),但是对于两个数大于 \(n\) 的情况我们多减去了三次,所以还要加回来,最后的方案数就是
\[{sum-1\choose 2}-{sum-3n-1\choose 2}-3{sum-n-1\choose 2}+3{sum-2n-1\choose 2} \]化简之后就是上面的东西了。
§ 优化贰:\(\mathcal O(1)\) §
如果得到了前缀和,那么我们就可以使用二分了。
考虑再对 \([x^i]g(x)\) 求关于 \(i\) 的前缀和,设 \([x^i]g(x)\) 的生成函数为 \(\sigma\),则有
考虑将 \(\sigma\) 与单位向量 \(I\) 卷起来:
对 \(\sigma\cdot I\) 进行麦克劳林展开:
定义 \(\alpha(x)=x(x+1)(x+2)/6\),那么 \(I\cdot \sigma\) 的通项公式即为 \(\alpha(x-2)-3\alpha(x-n-2)+3\alpha(x-2n-2)\)
这个东西在 \((0,+\infty)\) 单增,那么我们可以考虑不等式,即有如下不等式:
我们真的没有使用在线计算器:)
对于后面的 \(i,j\) 也可以使用这种简单的不等式在 \(\mathcal O(1)\) 内推导。
所以总时间复杂度为 \(\mathcal O(1)\),代码实现 \(\mathcal O(+\infty)\).
叁、参考代码 ¶
只有背包的代码咯:
for(LL i=1;i<=n;i++)dp[1][i]=1LL,dp[1][i]+=dp[1][i-1];
for(LL i=n+1;i<=3*n;i++)dp[1][i]=dp[1][n];
for(LL i=2;i<=2*n;i++)dp[2][i]+=dp[1][i-1]-dp[1][max(0LL,i-n-1LL)],dp[2][i]+=dp[2][i-1];
for(LL i=2*n+1;i<=3*n;i++)dp[2][i]=dp[2][2*n];
for(LL i=3;i<=3*n;i++)dp[3][i]+=dp[2][i-1]-dp[2][max(0LL,i-n-1LL)];
for(LL i=3;i<=3*n;i++){
if(k<=dp[3][i]){
for(LL j=1;j<=n;j++){
if(i-j>2*n)continue;
LL down=max(1LL,i-j-n),up=min(n,i-j-1);
if(k>up-down+1LL)k-=(up-down+1LL);
else{
printf("%lld %lld %lld\n",j,down+k-1LL,i-j-down-k+1LL);
return 0;
}
}
}
else k-=dp[3][i];
}