「2019纪中集训Day4」解题报告

T1、forging

勇者虽然武力值很高,但在经历了多次战斗后,发现怪物越来越难打。
于是开始思考是不是自己平时锻炼没到位,于是苦练一个月后发现......自己连一个史莱姆都打不过了。
勇者的精灵路由器告诉勇者其实是他自己的武器不好,并把他指引到了锻造厂。
“欢迎啊,老朋友。”
一阵寒暄过后,厂长带他们参观了厂子四周,并给他们讲锻造的流程。
“我们这里的武器分成若干的等级,等级越高武器就越厉害,并且对每一等级的武器都有两种属性值 \(b\)\(c\),但是我们初始只能花 \(a\) 个金币来生产 \(1\)\(0\) 级剑......”
“你们厂子怎么这么垃圾啊,不能一下子就造出来 \(999\) 级的武器吗?”勇者不耐烦的打断了厂长的话。
“别着急,还没开始讲锻造呢......举例你手中有一把 \(x\) 级武器和一把 \(y\) 级武器 \((y = \max(x - 1, 0))\),令锻造附加值 \(k = \min(c_x , b_y)\),则有 \(\frac{k}{c_x}\) 的概率将两把武器融合成一把 \(x + 1\) 级的武器。”
“......但是,锻造不是一帆风顺的,你同样有 \(1 - \frac{k}{c_x}\) 的概率将两把武器融合成一把 \(\max(x - 1, 0)\) 级的武器......”
勇者听完后暗暗思忖,他知道厂长一定又想借此机会坑骗他的零花钱,于是求助这个村最聪明的智者——你,来告诉他,想要强化出一把 \(n(n \le 10^7)\) 级的武器,其期望花费为多少?
由于勇者不精通高精度小数,所以你只需要将答案对 \(998244353\) ( \(7 \times17 \times 2^{23} + 1\),一个质数 ) 取模即可。

\(Sol\)

\(f_i\) 表示造出一把 \(i\) 级武器的期望代价,\(p_i\) 表示强化 \(i\) 武器时成功的概率;
通过推导不难得出:\(f_i = \frac{1}{p} f_{i - 1} + f_{i - 2}\)
要注意常数因子带来的影响。

\(Source\)

#include <cstdio>
int in() {
	int x = 0; char c = getchar(); bool f = c == '-';
	while (c < '0' || c > '9')
		f |= c == '-', c = getchar();
	while (c >= '0' && c <= '9')
		x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
	return f ? -x : x;
}
template<typename T>inline void chk_min(T &_, T __) { _ = _ < __ ? _ : __; }
template<typename T>inline void chk_max(T &_, T __) { _ = _ > __ ? _ : __; }

const int N = 1e7 + 5, mod = 998244353;

int n, A, a[N], b[N], c[N], f[N], inv[N];

int qpow(int base, int b, int ret = 1) {
	for (; b; b >>= 1, base = 1ll * base * base % mod)
		if (b & 1)
			ret = 1ll * ret * base % mod;
	return ret;
}

inline int min(int _, int __) { return _ < __ ? _ : __; }
inline int max(int _, int __) { return _ > __ ? _ : __; }

int main() {
//	freopen("in", "r", stdin);
	freopen("forging.in", "r", stdin);
	freopen("forging.out", "w", stdout);
	n = in(), A = in();
	int bx = in(), by = in(), cx = in(), cy = in(), p = in();
	b[0] = by + 1, c[0] = cy + 1;
	for(int i = 1; i < n; ++i) {
		b[i] = (1ll * b[i - 1] * bx + by) % p + 1;
		c[i] = (1ll * c[i - 1] * cx + cy) % p + 1;
	}
	inv[1] = 1;
	for (int i = 2; i <= p; ++i)
		inv[i] = 1ll * (mod - (mod / i)) * inv[mod % i] % mod;
	f[0] = A;
	for (int i = 1; i <= n; ++i) {
		f[i] = 1ll * inv[min(b[max(i - 2, 0)], c[i - 1])] * c[i - 1] % mod * f[i - 1] % mod + f[max(i - 2, 0)];
		if (f[i] >= mod)
			f[i] -= mod;
	}
	printf("%d\n", f[n]);
	return 0;
}

T2、division

整除符号为 \(|\)\(d|n\) 在计算机语言中可被描述为 \(n \% d == 0\)
现有一算式 \(n | x^m − x\),给定 \(n\)\(m\),求 \([1, n]\) 以内 \(x\) 解的个数。
解可能很大,输出取模 \(998244353\)

其中 \(n\) 的给定方式是由 \(c\) 个不超过 \(t\) 的质数的乘积给出的,\(c(c \le 10 ^ 4)\)\(t\) 的范围会在数据范围中给出。
第一行一个 \(id\) 表示这个数据点的标号。
多组数据,其中第二行一个整数 \(T\) 表示数据组数。
对于每一组数据:
第一行两个整数 \(c\)\(m(m \le 10 ^ 9)\)
第二行 \(c\) 个整数 \(p_i (p_i \le 10 ^4)\),这些整数都是质数,且两两不同,他们的乘积即为\(n\)
由于你可以通过输入求出 \(t\),输入不再给出。

\(Sol_1\)

首先根据题意可以得到 \(c\) 个形如 \(x ^ m \equiv x \ (mod \ p_i)\) 方程组:
对于第 \(i\) 个方程,可以暴力枚举 \(p_i\) 以内的所有正整数,设得到的解的个数为 \(t_i\)
答案为 \(\Pi t_i\),时间复杂度 \(O(T \ c \ p_i)\)

简单证明:
对于每个 \(i\)\(t_i\) 个解可以看做 \(t_i\) 个形如 \(s_{i,j} \equiv 0 \ (mod \ p_i)\) 的方程组;
现在对于每一个 \(i\) 都选出一个解(即上述方程组中的一个),可以得到一个新的方程组;
这个新的方程组在 \(n\) 以内只有一个正整数解,\(QED\)

\(Source\)

//#pragma GCC optimize(2)
#pragma GCC optimize(3,"Ofast","inline")
#include <cstdio>
int in() {
	int x = 0; char c = getchar(); bool f = c == '-';
	while (c < '0' || c > '9')
		f |= c == '-', c = getchar();
	while (c >= '0' && c <= '9')
		x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
	return f ? -x : x;
}
template<typename T>inline void chk_min(T &_, T __) { _ = _ < __ ? _ : __; }
template<typename T>inline void chk_max(T &_, T __) { _ = _ > __ ? _ : __; }

int c, n, m;

const int N = 1e4 + 5, mod = 998244353;
int f[N], min_prime[N], pri[N];
void init_prime() {
	for (int i = 2; i < N; ++i) {
		if (!min_prime[i]) {
			min_prime[i] = i;
			pri[++pri[0]] = i;
		}
		for (int j = 1; j <= pri[0] && i * pri[j] < N; ++j) {
			min_prime[i * pri[j]] = pri[j];
			if (i % pri[j] == 0)
				break;
		}
	}
}

int qpow(int base, int b, int ret = 1, int p = mod) {
	for (; b; b >>= 1, base = 1ll * base * base % p)
		if (b & 1)
			ret = 1ll * ret * base % p;
	return ret;
}

int main() {
//	freopen("in", "r", stdin);
	freopen("division.in", "r", stdin);
	freopen("division.out", "w", stdout);
	int id = in(), T = in();
	init_prime();
	while (T--) {
		c = in(), m = in();
		int res = 1;
		for (int i = 1; i <= c; ++i) {
			n = in();
			int tmp = 2;
			f[1] = 1;
			for (int i = 2; i < n; ++i) {
				if (i == min_prime[i])
					f[i] = qpow(i, m, 1, n);
				else
					f[i] = 1ll * f[min_prime[i]] * f[i / min_prime[i]] % n;
				if (f[i] == i)
					++tmp;
			}
			res = 1ll * res * tmp % mod;
		}
		printf("%d\n", res);
	}
	return 0;
}

\(p.s.\)关于这个做法好像假了,我这两天再证一下 (2019-08-06)。

\(Sol_2\)

由于 \(p\) 是质数,\([1,p-1]\) 的所有整数都可以表示为 \(g ^ y\)
所以方程组可以写成 \(g^{my} \equiv g^y \ (mod \ (p - 1))\)
根据费马小定理:

\[my \equiv y \ (mod \ (p - 1)) \Rightarrow (m - 1)y \equiv 0 \ (mod \ (p - 1)) \]

两边同时除以 \((m - 1, p - 1)\),令 \(d=(m - 1, p - 1)\)

\[\frac{m-1}{d} y \equiv 0 \ (mod \ \frac{p - 1}{d}) \]

由于 \(y \in [0, p - 2]\),所以 \(\frac{p - 1}{d}\) 的任意小于 \(d\) 的非负整数倍都符合条件;
所以第 \(i\) 个方程的解个数为 \(d + 1\)\(x = p\) 显然是唯一一个没有被算到的解。
时间复杂度 \(O(T \ c \ log p)\)

\(Source\)

#include <cstdio>
int in() {
	int x = 0; char c = getchar(); bool f = c == '-';
	while (c < '0' || c > '9')
		f |= c == '-', c = getchar();
	while (c >= '0' && c <= '9')
		x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
	return f ? -x : x;
}
template<typename T>inline void chk_min(T &_, T __) { _ = _ < __ ? _ : __; }
template<typename T>inline void chk_max(T &_, T __) { _ = _ > __ ? _ : __; }

const int mod = 998244353;

int c, n, m;

int gcd(int a, int b) {
	return b ? gcd(b, a % b) : a;
}

int main() {
//	freopen("in", "r", stdin);
	freopen("division.in", "r", stdin);
	freopen("division.out", "w", stdout);
	int id = in(), T = in();
	while (T--) {
		c = in(), m = in();
		int res = 1;
		for (int i = 1; i <= c; ++i)
			res = 1ll * res * (gcd(in() - 1, m - 1) + 1) % mod;
		printf("%d\n", res);
	}
	return 0;
}

T3、money

\(n(n \le 10 ^ 5)\) 个点,\(m(m \le 10 ^ 6)\) 次操作;
有两种操作:
\(0\)\(a\)\(b\) 连一条边权为 \(c\) 的有向边;
\(1\):查询 \(a\)\(b\) 的通路上的最小值,若没有通路则输出 \(0\)
保证每个点只会连出去一条边,强制在线。

\(Sol_1\)

因为最后一句话的限制,显然这是一棵树,且方向为儿子到父亲,考虑倍增;
每次合并两棵树时考虑启发式合并(重新计算点数较小的树的倍增数组),此时当做无根树,还要记录一下是否是父亲走向儿子的边;
由于每个点只有在该点所在联通块的大小 更新为至少原来的两倍时 才会重新计算它的倍增数组;
所以时间复杂度为 \(O(n \ log ^2 n + m \ log n)\)
没写,所以没有代码;

\(Sol_2\)

因为倍增数组里的信息只会增加,所以只要做到添加信息时做到不重复,时间复杂度仍是 \(O(nlogn)\)
对于一棵树,可以用 \(vector\) 等数据结构来维护同一深度的点;
对于 \(u\) 子树中每一层节点,记录每一层已经有 \(k\) 级祖先的信息,每次连一条 \(u\) -> \(v\) 的边 (\(u\) 为子节点),从 \(k + 1\) 级祖先开始增加信息即可;
没写。

\(Sol_3\)

\(lct\) 裸题。
\(lct\) 维护 \(a,b\) 两点的 \(lca\)\(access(a)\)\(access(b)\) 时最后一次进行 \(splay\) 操作的点就是 \(lca\)
时间复杂度 \(O(m \ logn)\),由于 \(lct\) 的超大常数,考场只拿了 \(80\) 分,要大力卡常开 \(O_3\)才能过(第一次加输出优化变快,快了近 \(2.1s\))。

\(Source\)

//#pragma GCC optimize(2)
#pragma GCC optimize(3,"Ofast","inline")
#include <cstdio>
#include <cstring>
inline char get_char(){
    static char buf[100000],*p1=buf,*p2=buf;
    return p1==p2&&(p2=(p1=buf)+fread(buf,1,100000,stdin),p1==p2)?EOF:*p1++;
}
int in() {
	register int x = 0; register char c = get_char();
	while (c < '0' || c > '9')
		c = get_char();
	while (c >= '0' && c <= '9')
		x = (x << 1) + (x << 3) + (c ^ 48), c = get_char();
	return x;
}
void out(register int x) {
	if (x > 9)
		out(x / 10);
	putchar(x % 10 + '0');
}
inline void out_ln(register int x) {
	out(x), puts("");
}
template<typename T>inline void chk_min(T &_, T __) { _ = _ < __ ? _ : __; }
template<typename T>inline void chk_max(T &_, T __) { _ = _ > __ ? _ : __; }

const int N = 1e5 + 5, inf = 0x3f3f3f3f;

int n, m, lastans, pre[N];

int get_fa(int u) {
	return pre[u] == u ? u : pre[u] = get_fa(pre[u]);
}

//link_cut_tree begin
int fa[N], c[N][2], val[N], min[N];
bool rev[N];
inline bool nroot(register int p) {
	return c[fa[p]][0] == p || c[fa[p]][1] == p;
}
inline void push_up(register int p) {
	min[p] = val[p];
	chk_min(min[p], min[c[p][0]]);
	chk_min(min[p], min[c[p][1]]);
}
inline void rever(register int p) {
	register int t = c[p][0]; c[p][0] = c[p][1], c[p][1] = t;
	rev[p] ^= 1;
}
inline void push_down(register int p) {
	if (rev[p]) {
		if (c[p][0])
			rever(c[p][0]);
		if (c[p][1])
			rever(c[p][1]);
		rev[p] = 0;
	}
}
void push_all(int p) {
	if (nroot(p))
		push_all(fa[p]);
	push_down(p);
}
void rotate(register int x) {
	register int y = fa[x], z = fa[y], l = c[y][1] == x, r = l ^ 1;
	if (nroot(y))
		c[z][c[z][1] == y] = x;
	fa[x] = z, fa[y] = x;
	if (c[x][r])
		fa[c[x][r]] = y;
	c[y][l] = c[x][r], c[x][r] = y;
	push_up(y), push_up(x);
}
void splay(register int x) {
	register int y, z;
	push_all(x);
	while (nroot(x)) {
		y = fa[x], z = fa[y];
		if (nroot(y))
			rotate(c[y][0] == x ^ c[z][0] == y ? x : y);
		rotate(x);
	}
}

int access(register int p) {
	register int ret;
	for (register int t = 0; p; p = fa[t = p])
		splay(p), c[p][1] = t, push_up(p), ret = p;
	return ret;
}
inline void make_root(register int p) {
	access(p), splay(p), rever(p);
}
inline void split(register int p, register int t) {
	make_root(p), access(t), splay(t);
}
inline void link(register int p, register int t) {
	splay(p), fa[p] = t;
	val[p] = (in() + lastans) % n + 1;
	chk_min(min[p], val[p]);
}
inline int query(register int a, register int b) {
	register int ret = 0;
	access(b);
	if (access(a) == b) {
		make_root(b);
		access(a), splay(b);
		ret = min[c[b][1]];
		make_root(get_fa(b));
	}
	return ret;
}
//link_cut_tree end

int main() {
//	freopen("in", "r", stdin);
	freopen("money.in", "r", stdin);
	freopen("money.out", "w", stdout);
	n = in(), m = in();
	for (register int i = 1; i <= n; ++i)
		pre[i] = i;
	memset(val, inf, sizeof(val));
	min[0] = val[0];
	while (m--) {
		if (in()) {
			int a = (in() + lastans) % n + 1, b = (in() + lastans) % n + 1;
//			printf("%d %d\n", a, b);
			if (get_fa(a) == get_fa(b))
				lastans = query(a, b);
			else
				lastans = 0;
			out_ln(lastans);
		} else {
			int a = (in() + lastans) % n + 1, b = (in() + lastans) % n + 1;
			link(a, b);
			pre[a] = b;
		}
	}
	return 0;
}
posted @ 2019-08-04 21:25  15owzLy1  阅读(207)  评论(0编辑  收藏  举报