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
", 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 imes 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
", 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 imes 2 = 4) 丢回去,以此类推。

一直这样添加下去,并且保证代价和不超过 (n),能添加的最多次数就是答案(如果代价小于 (n),容易发现此时在数列中出现过的最大元素只出现了一次,将它增大即可保证数列总和正好为 (n))。

但是因为答案太大了不能直接模拟,因为每次代价增加量总是递增的,我们可以二分最后添加的代价是多少。

假设要把代价为 (1 sim x) 的所有增加量都加满,此时总代价为 (displaystyle sum_{i = 0}^{log_2 x} frac{leftlfloor frac{x}{2^i} ight floor left( leftlfloor frac{x}{2^i} ight floor + 1 ight)}{2} cdot 2^i),根据这个值与 (n) 的大小关系二分即可。

D. Do I Wanna Know?

题意简述

有个 (n) 个点的竞赛图。并给出概率 (p = a / b),图中每条有向边的方向是这样确定的:

  • 结点 (i, j)(1 le i < j le n))之间的有向边的方向,有 (p) 的概率为 (i o j),另外 (1 - p) 的概率为 (j o 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 ight) mod 998244353)

数据范围:(2 le n le 6 imes {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
", 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} ight)}^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 rack k}_q = sum_{i = 0}^{k - 1} frac{1 - q^{n - i}}{1 - q^{i + 1}}) 递推计算每个 (displaystyle {n rack 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))

[egin{aligned} F_k(qz) &= q^k z^k prod_{i = 0}^{k} frac{1}{1 - q^{i + 1} z} \ &= q^k frac{1 - z}{1 - q^{k + 1} z} F_k(z) \ (1 - q^{k + 1} z) F_k(qz) &= q^k (1 - z) F_k(z) \ [z^n] (1 - q^{k + 1} z) F_k(qz) &= [z^n] q^k (1 - z) F_k(z) \ q^n mathrm{dp}[n][k] - q^{k + n} mathrm{dp}[n - 1][k] &= q^k mathrm{dp}[n][k] - q^k mathrm{dp}[n - 1][k] \ (q^n - q^k) mathrm{dp}[n][k] &= (q^{k + n} - q^k) mathrm{dp}[n - 1][k] \ mathrm{dp}[n][k] &= frac{1 - q^n}{1 - q^{n - k}} mathrm{dp}[n - 1][k] \ &= prod_{i = 1}^{n - k} frac{1 - q^{i + k}}{1 - q^i} \ end{aligned} ]

好吧,这实际上和上面的式子等价,也就是我们证了一遍 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 imes {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
", Ans);
	return 0;
}

轨迹是个一次函数,如果枚举轨迹的斜率(显然必须是个整数),那么可行的截距是一个区间,其左右端点如何确定呢?

以左端点为例,假设斜率为 (k),左端点应该被 (displaystyle i = mathop{argmax}_{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
",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 ight))

改成 (displaystyle sum_{forall 1 le i le k, x_i in {0, 1, 2}} prod_{j = 1}^{k} {(-1)}^{x_i} inom{2}{x_i} left( 2^{alpha_i + 1 - x_i} - 1 ight)) 可以在 (mathcal O !left( 3^{omega(n)} ight)) 的时间复杂度内计算。

来自 PinkRabbit 的博客园(https://www.cnblogs.com/PinkRabbit/)。未经允许,请勿转载。
原文地址:https://www.cnblogs.com/PinkRabbit/p/OpenCup-XVIII-GP-of-Gomel.html