XVIII Open Cup GP of Gomel(待更新)
A. Aftermath
题意简述
有一个正整数 \(n\),给出它的所有因数的算术平均数 \(a\) 和调和平均数 \(h\),它们恰好都是整数,请你还原出任意一个满足条件的 \(n\) 的值。
数据范围:保证存在一个合法的 \(n\) 使得 \(1 \le n \le {10}^{15}\)。
AC 代码
#include <cstdio>
typedef long long LL;
LL A, H;
int main() {
scanf("%lld%lld", &A, &H);
printf("%lld\n", A * H);
return 0;
}
$ \displaystyle \frac{1}{h} = \frac{\displaystyle \sum_{d \mathrel{|} n} d^{-1}}{\sigma_0 (n)} = \frac{\displaystyle \sum_{d \mathrel{|} n} \frac{n}{d}}{n \sigma_0 (n)} = \frac{1}{n} \frac{\displaystyle \sum_{d \mathrel{|} n} d}{\sigma_0 (n)} = \frac{a}{n} $,所以 \(n = a \times h\)。
B. Believer
题意简述
定义一个正整数数列的权值为所有数在数列中的出现次数的 popcount 之和。
给定 \(n\),请求出数列中元素总和为 \(n\) 的正整数数列的权值的最大值。
有 \(T\) 组数据。
数据范围:\(1 \le T \le {10}^3\),\(1 \le n \le {10}^{18}\)。
AC 代码
#include <cstdio>
typedef long long LL;
LL N;
int main() {
int Tests;
scanf("%d", &Tests);
while (Tests--) {
scanf("%lld", &N);
LL l = 1, r = 1.5e9, ans = 0;
while (l <= r) {
LL mid = (l + r) / 2;
LL x = mid, b = 1, sum = 0;
while (x) {
sum += (x * (x + 1) / 2) * b;
x /= 2, b *= 2;
}
if (sum <= N) ans = mid, l = mid + 1;
else r = mid - 1;
}
LL x = ans, b = 1, sum = 0, cnt = 0;
while (x) {
sum += (x * (x + 1) / 2) * b;
cnt += x;
x /= 2, b *= 2;
}
cnt += (N - sum) / (ans + 1);
printf("%lld\n", cnt);
}
return 0;
}
我们注意到 popcount 取到极大值的时候,也就是 \(2^k - 1\) 的 popcount 为 \(k\)。
也就是说,如果我们想要通过添加数 \(x\) 让数列权值逐渐增加,初始时没有 \(x\),第一次添加 \(1\) 个 \(x\) 让数列权值增加 \(1\),第二次需要添加 \(2\) 个 \(x\) 让数列权值再增加 \(1\),第 \(k\) 次增加的时候需要在之前的基础上添加 \(2^{k - 1}\) 个 \(x\),也就是代价是逐渐增大的。
对于这种代价逐渐增大的问题,我们只需要贪心就行了。相当于现在有 \(\{1, 2, 3, 4, \ldots \}\) 可以进行添加,我们肯定先选代价最小的 \(1\) 添加进去,但是下一次添加 \(1\) 就必须一次添加 \(2\) 个 \(1\) 了,所以我们再把 \(2\) 倍的 \(1\) 丢回去,也就是变成了 \(\{2, 2, 3, 4, \ldots \}\),此时序列的权值就为 \(1\)。想要再让序列权值增加 \(1\),我们只能选择 \(2\) 的代价,然后把 \(2 \times 2 = 4\) 丢回去,以此类推。
一直这样添加下去,并且保证代价和不超过 \(n\),能添加的最多次数就是答案(如果代价小于 \(n\),容易发现此时在数列中出现过的最大元素只出现了一次,将它增大即可保证数列总和正好为 \(n\))。
但是因为答案太大了不能直接模拟,因为每次代价增加量总是递增的,我们可以二分最后添加的代价是多少。
假设要把代价为 \(1 \sim x\) 的所有增加量都加满,此时总代价为 \(\displaystyle \sum_{i = 0}^{\log_2 x} \frac{\left\lfloor \frac{x}{2^i} \right\rfloor \left( \left\lfloor \frac{x}{2^i} \right\rfloor + 1 \right)}{2} \cdot 2^i\),根据这个值与 \(n\) 的大小关系二分即可。
D. Do I Wanna Know?
题意简述
有个 \(n\) 个点的竞赛图。并给出概率 \(p = a / b\),图中每条有向边的方向是这样确定的:
-
结点 \(i, j\)(\(1 \le i < j \le n\))之间的有向边的方向,有 \(p\) 的概率为 \(i \to j\),另外 \(1 - p\) 的概率为 \(j \to i\)。
-
并且每条边的方向是两两独立的随机变量。
定义 \(f_k\)(\(1 \le k \le n - 1\))表示这个竞赛图中存在大小为 \(k\) 的点集 \(S\),满足「对于每对 \(S\) 中的点与不在 \(S\) 中的点,它们之间的有向边都是从 \(S\) 中的点出发的」的概率。
再定义 \(g_k = 1\),以及 \(g_k = {(g_{k - 1})}^2 + 2\)(\(2 \le k \le n - 1\))。
请求出 \(\left( \sum_{k = 1}^{n - 1} f_k g_k \right) \bmod 998244353\)。
数据范围:\(2 \le n \le 6 \times {10}^5\),\(1 \le a < b \le 100\)。
AC 代码
#include <cstdio>
typedef long long LL;
const int Mod = 998244353;
inline int qPow(int b, int e) {
int a = 1;
for (; e; e >>= 1, b = (LL)b * b % Mod)
if (e & 1) a = (LL)a * b % Mod;
return a;
}
inline int gInv(int x) { return qPow(x, Mod - 2); }
int N, Pr, p;
int Ans;
int main() {
int a, b;
scanf("%d%d%d", &N, &a, &b);
Pr = (LL)a * gInv(b) % Mod;
p = (LL)(1 - Pr + Mod) * gInv(Pr) % Mod;
int Val = 1, gVal = 1;
for (int i = 1; i <= N - 1; ++i) {
if (p == 1) Val = (LL)Val * (N - i + 1) % Mod * gInv(i) % Mod;
else Val = (LL)Val * (1 - qPow(p, N - i + 1) + Mod) % Mod * gInv(1 - qPow(p, i) + Mod) % Mod;
Ans = (Ans + (LL)gVal * Val % Mod * qPow(Pr, (LL)i * (N - i) % (Mod - 1))) % Mod;
gVal = ((LL)gVal * gVal + 2) % Mod;
}
printf("%d\n", Ans);
return 0;
}
看起来 \(g\) 没有什么性质,那么我们需要求出所有的 \(f_k\)。
我们知道竞赛图的一个性质:强连通分量缩点后,竞赛图形成一条链的形式。
可以推出如果存在大小为 \(k\) 的集合,那么恰好存在一个。
根据期望的线性性,也就是要对每个 \(k\),询问每个大小为 \(k\) 的点集 \(S\),\(S\) 到其他点的有向边全都由 \(S\) 侧出发的概率。
这也就是,对于每对 \(u \in S\) 和 \(v \in (V \setminus S)\),如果 \(u < v\) 则贡献 \(p\) 到乘积中,否则贡献 \(1 - p\) 到乘积中。
也就是说,对于每个 \(S\),概率都是 \(p^{k (n - k) - t} {(1 - p)}^{t}\) 的形式,改写为 \(\displaystyle p^{k (n - k)} \cdot {\left( \frac{1 - p}{p} \right)}^t\)。
对每个 \(k\) 提出 \(p^{k (n - k)}\),再令 \(\displaystyle q = \frac{1 - p}{p}\),只需计算所有 \(S\) 的方案中的 \(q^t\) 之和即可。
这实际上等价于:枚举所有长度为 \(n\),且有 \(k\) 个 \(1\) 的 \(01\) 串,记它的逆序对个数为 \(t\),将所有 \(q^t\) 求和。
于是可以很简单地进行 DP:令 \(\mathrm{dp}[n][k]\) 表示上述取值,则有:
\(\mathrm{dp}[n][k] = q^k \mathrm{dp}[n - 1][k] + \mathrm{dp}[n - 1][k - 1]\),也就是枚举最后一个数是 \(0\) 还是 \(1\)。
边界条件是 \(\mathrm{dp}[0][0] = 1\),且所有 \(n < 0\) 或 \(k < 0\) 或 \(k > n\) 的位置为 \(0\)。
这实际上就是 q-二项式系数(q-Binomial Coefficient)的递推公式。根据 \(\displaystyle {n \brack k}_q = \sum_{i = 0}^{k - 1} \frac{1 - q^{n - i}}{1 - q^{i + 1}}\) 递推计算每个 \(\displaystyle {n \brack k}_q\) 即可。
当然也有其他方法,如果令 \(\displaystyle F_k(z) = \sum_{n = 0}^{\infty} \mathrm{dp}[n][k] z^n\),也就是每一列的生成函数,我们有:
直接根据递推式有 \(F_k(z) = q^k z F_k(z) + z F_{k - 1}(z)\),也就是 \(\displaystyle F_k(z) = \frac{z F_{k - 1}(z)}{1 - q^k z}\)。
根据边界条件 \(\displaystyle F_0(z) = \frac{1}{1 - z}\),可以得到 \(\displaystyle F_k(z) = z^k \prod_{i = 0}^{k} \frac{1}{1 - q^i z}\)。我们要求的是每个 \([z^n] F_k(z)\)。
好吧,这实际上和上面的式子等价,也就是我们证了一遍 q-二项式系数的通项公式。
感谢 EntropyIncreaser 提供的帮助。
F. Forever and Always
G. Gate 21
题意简述
平面上有 \(n\) 条线段,第 \(i\) 条线段连接两点 \((i, l_i)\) 和 \((i, r_i)\)(\(l_i \le r_i\))。
有一个点的轨迹是一条直线,定义这条轨迹合法,当且仅当它在每一条线段上都经过了一个整点。
请你计算合法的轨迹的数量。
数据范围:\(1 \le n \le 2 \times {10}^5\),\(1 \le l_i \le r_i \le {10}^9\)。
AC 代码
#include <cstdio>
#include <algorithm>
typedef long long LL;
const LL Inf = 0x0000003f3f3f3f3f;
const int MN = 200005;
LL Div(LL x, int y) {
int z = (int)(x % y);
if (z < 0) z += y;
return (x - z) / y;
}
int N; LL A[MN], B[MN];
int sA[MN], cA;
LL lA[MN], rA[MN];
int sB[MN], cB;
LL lB[MN], rB[MN];
int stk[MN], tp;
int main() {
scanf("%d", &N);
for (int i = 1; i <= N; ++i) scanf("%lld%lld", &A[i], &B[i]);
stk[tp = 1] = N;
for (int i = N - 1; i >= 1; --i) {
while (tp >= 2 && (A[stk[tp - 1]] - A[stk[tp]]) * (stk[tp] - i) >= (A[stk[tp]] - A[i]) * (stk[tp - 1] - stk[tp])) --tp;
stk[++tp] = i;
}
cA = tp;
for (int i = 1; i <= cA; ++i) sA[i] = stk[i];
lA[1] = -Inf, rA[cA] = Inf;
for (int i = 2; i <= cA; ++i) {
LL v = Div(A[sA[i - 1]] - A[sA[i]], sA[i - 1] - sA[i]);
rA[i - 1] = v, lA[i] = v + 1;
}
stk[tp = 1] = 1;
for (int i = 2; i <= N; ++i) {
while (tp >= 2 && (B[stk[tp]] - B[stk[tp - 1]]) * (i - stk[tp]) >= (B[i] - B[stk[tp]]) * (stk[tp] - stk[tp - 1])) --tp;
stk[++tp] = i;
}
cB = tp;
for (int i = 1; i <= cB; ++i) sB[i] = stk[i];
lB[1] = -Inf, rB[cB] = Inf;
for (int i = 2; i <= cB; ++i) {
LL v = Div(B[sB[i]] - B[sB[i - 1]], sB[i] - sB[i - 1]);
rB[i - 1] = v, lB[i] = v + 1;
}
LL Ans = 0;
int i = 1, j = 1;
while (i <= cA && j <= cB) {
if (lA[i] > rA[i]) { ++i; continue; }
if (lB[j] > rB[j]) { ++j; continue; }
if (rA[i] < lB[j]) { ++i; continue; }
if (rB[j] < lA[i]) { ++j; continue; }
LL lb = std::max(lA[i], lB[j]), rb = std::min(rA[i], rB[j]);
if (sA[i] < sB[j]) rb = std::min(rb, Div(B[sB[j]] - A[sA[i]], sB[j] - sA[i]));
if (sA[i] > sB[j]) lb = std::max(lb, Div(A[sA[i]] - B[sB[j]] - 1, sA[i] - sB[j]) + 1);
if (lb <= rb) {
LL lbs = (B[sB[j]] - lb * sB[j]) - (A[sA[i]] - lb * sA[i]) + 1;
LL rbs = (B[sB[j]] - rb * sB[j]) - (A[sA[i]] - rb * sA[i]) + 1;
Ans += (lbs + rbs) * (rb - lb + 1) / 2;
}
++(rA[i] <= rB[j] ? i : j);
}
printf("%lld\n", Ans);
return 0;
}
轨迹是个一次函数,如果枚举轨迹的斜率(显然必须是个整数),那么可行的截距是一个区间,其左右端点如何确定呢?
以左端点为例,假设斜率为 \(k\),左端点应该被 \(\displaystyle i = \mathop{\arg\max}_{j = 1}^{n} (l_j - k \cdot j)\) 这个 \((i, l_i)\) 限制住。
这是一个经典的凸壳形式,求出所有 \((i, l_i)\) 的上凸壳即可。对于右端点,则是所有 \((i, r_i)\) 的下凸壳。
然后在凸壳上双指针即可,每一段左端点和右端点对应的截距是一个一次函数,等差数列求和即可。
注意实现细节。
K. Kids Aren’t Alright
题意简述
给定 \(m\),求出集合内元素的 \(\gcd\) 为 \(1\) 而 \(\operatorname{lcm}\) 为 \(n\) 的非空集合的个数,对 \(998244353\) 取模。
数据范围:\(1 \le m \le {10}^{18}\)。
AC 代码
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define RI int
#define RL LL
#define Con const
#define CI Con int&
#define CL Con LL&
#define I inline
#define W while
#define LL long long
#define Gmax(x,y) (x<(y)&&(x=(y)))
#define abs(x) ((x)<0?-(x):(x))
#define hl_AK_NOI true
using namespace std;
I LL Qmul(CL x,CL y,CL X)//快速乘
{
RL k=(LL)((1.0L*x*y)/(1.0L*X)),t=x*y-k*X;
t-=X;W(t<0) t+=X;return t;
}
class MillerRabin//MillerRabin判素数板子
{
private:
#define Pcnt 12
Con int P[Pcnt]={2,3,5,7,11,13,17,19,61,2333,4567,24251};
I LL Qpow(RL x,RL y,CL X)
{
RL t=1;W(y) y&1&&(t=Qmul(t,x,X)),x=Qmul(x,x,X),y>>=1;
return t;
}
I bool Check(CL x,CI p)
{
if(!(x%p)||Qpow(p%x,x-1,x)^1) return false;
RL k=x-1,t;W(!(k&1))
{
if((t=Qpow(p%x,k>>=1,x))^1&&t^(x-1)) return false;
if(!(t^(x-1))) return true;
}return true;
}
public:
bool IsPrime(CL x)
{
if(x<2) return false;
for(RI i=0;i^Pcnt;++i) {if(!(x^P[i])) return true;if(!Check(x,P[i])) return false;}
return true;
}
};
const int MC=105;
int c;
LL ar[MC];
class PollardRho//PollardRho分解质因数
{
private:
#define Rand(x) (1LL*rand()*rand()*rand()*rand()%(x)+1)
MillerRabin MR;
I LL gcd(CL x,CL y) {return y?gcd(y,x%y):x;}//求gcd
I LL Work(CL x,CI y)//分解
{
RI t=0,k=1;RL v0=Rand(x-1),v=v0,d,s=1;W(hl_AK_NOI)//初始化随机一个v0
{
if(v=(Qmul(v,v,x)+y)%x,s=Qmul(s,abs(v-v0),x),!(v^v0)||!s) return x;//计算当前v,统计乘积,判断是否分解失败
if(++t==k) {if((d=gcd(s,x))^1) return d;v0=v,k<<=1;}//倍增
}
}
I void Resolve(RL x,RI t)//分解
{
if(x==1)return;if(MR.IsPrime(x))return ar[++c]=x,void();
RL y=x;W((y=Work(x,t--))==x);x/=y;Resolve(x,t),Resolve(y,t);//找到一个因数y,然后分解(注意除尽)
}
public:
I PollardRho() {srand(20050521);}//初始化随机种子
I void Solve(CL x) {Resolve(x,302627441);}//求答案
}P;
const int Mod=998244353;
inline int qPow(int b,LL e){
int a=1;
for(;e;e>>=1,b=(LL)b*b%Mod)
if(e&1)a=(LL)a*b%Mod;
return a;
}
int b[MC],t;
int ans;
void DFS(int i,LL x,LL v){
if(i>t){
v=(v%Mod+Mod)%Mod;
ans=(ans+(LL)v*(qPow(2,x)-1))%Mod;
return;
}
DFS(i+1,x*(b[i]+1),v);
DFS(i+1,x*b[i],-2*v);
if(b[i]>1)DFS(i+1,x*(b[i]-1),v);
}
int main()
{
RL n;
scanf("%lld",&n);
P.Solve(n);
sort(ar+1,ar+c+1);
for(int i=1;i<=c;++i)
if(ar[i]!=ar[i-1])b[++t]=1;
else ++b[t];
// for(int i=1;i<=t;++i)printf("%d,",b[i]);
DFS(1,1,1);
printf("%d\n",ans);
return 0;
}
考察 \(\gcd\) 和 \(\operatorname{lcm}\) 的性质,显然只需知道每个质因子的次数,而不需要知道质因子本身。
反正我连 Miller–Rabin 都不会,直接抄了个 Pollard–Rho 板子分解了质因数。
实际上可以先筛掉 \({10}^6\) 内的质因数,然后剩下的只可能是 \(p\) 或者 \(p^2\) 或者 \(p q\) 三种形式之一,Miller–Rabin 判掉 \(p\) 形式,然后开个根判掉 \(p^2\) 就只剩 \(p q\) 不用判了。
得到了每个幂次 \(\alpha_1, \alpha_2, \ldots , \alpha_k\) 后,只需对每个质数幂次的 \(\gcd\) 和 \(\operatorname{lcm}\) 两个方向进行容斥。
也就是:\(\displaystyle \sum_{\forall 1 \le i \le k, (x_i, y_i) \in {(\{0, 1\})}^2} \prod_{j = 1}^{k} {(-1)}^{x_i} {(-1)}^{y_i} \left( 2^{\alpha_i + 1 - x_i - y_i} - 1 \right)\)。
改成 \(\displaystyle \sum_{\forall 1 \le i \le k, x_i \in \{0, 1, 2\}} \prod_{j = 1}^{k} {(-1)}^{x_i} \binom{2}{x_i} \left( 2^{\alpha_i + 1 - x_i} - 1 \right)\) 可以在 \(\mathcal O \!\left( 3^{\omega(n)} \right)\) 的时间复杂度内计算。