2022CCPC湖北省赛题解ABCFJKL

2022CCPC湖北省赛题解ABCFJKL

K. Keep Eating

题意

2022CCPC湖北省赛题解ABCFJKL_第1张图片

t    ( 1 ≤ t ≤ 1 e 5 ) t\ \ (1\leq t\leq 1\mathrm{e}5) t  (1t1e5)组测试数据.每组测试数据输入一个整数 n    ( 0 ≤ n ≤ 1001576 ) n\ \ (0\leq n\leq 1001576) n  (0n1001576)和一个实数 c    ( 1.0 ≤ c ≤ 11.5 ) c\ \ (1.0\leq c\leq 11.5) c  (1.0c11.5). n n n根据上述表格换算后与 c c c相加,输出结果.

代码 -> 2022CCPC湖北省赛-K(模拟)

void solve() {
	double n, ans; cin >> n >> ans;

	if (n >= 1e7) ans += 2;
	else if (n > 9800000) ans += 1.0 + (n - 9800000) / 200000;
	else ans += (n - 9500000) / 300000;

	cout << fixed << setprecision(18) << max(ans, 0.0) << endl;
}

int main() {
	CaseT  // 单测时注释掉该行
	solve();
}


B. Potion(easy version)

题意 ( 3   s 3\ \mathrm{s} 3 s)

2022CCPC湖北省赛题解ABCFJKL_第2张图片

有一容量为 ( a + b ) (a+b) (a+b)且只有一个从下往上的刻度 a a a的量杯.有两类水,若能通过若干次接水倒水使得杯中两种水的比例为 x : y x:y x:y,输出最小接水次数;否则输出 − 1 -1 1.简单版本中,保证 a = b = 1 a=b=1 a=b=1.

t    ( 1 ≤ t ≤ 1 e 5 ) t\ \ (1\leq t\leq 1\mathrm{e}5) t  (1t1e5)组测试数据.每组测试数据输入四个整数 x , y , a , b    ( 1 ≤ a , b , x , y ≤ 1 e 18 ) x,y,a,b\ \ (1\leq a,b,x,y\leq 1\mathrm{e}18) x,y,a,b  (1a,b,x,y1e18).数据保证 a = b = 1 a=b=1 a=b=1.

思路

设当前第一类水的比例为 d d d,初始时 d = 0 d=0 d=0 1 1 1.从刻度 a a a开始接满后,若接第一类水,则第一类水的比例变为 d + b a + b = d + 1 2 \dfrac{d+b}{a+b}=\dfrac{d+1}{2} a+bd+b=2d+1;若接第二类水,则第一类水的比例变为 d a + b = d 2 \dfrac{d}{a+b}=\dfrac{d}{2} a+bd=2d.

保持第一类水的比例为最简分数,规律: { d + 0 2 { d + 0 4 { d + 0 8 d + 4 8 d + 2 4 { d + 2 8 d + 6 8 d + 1 2 { d + 1 4 { d + 1 8 d + 5 8 d + 3 4 { d + 3 8 d + 7 8 \begin{cases}\dfrac{d+0}{2}\begin{cases}\dfrac{d+0}{4}\begin{cases}\dfrac{d+0}{8} \\ \dfrac{d+4}{8}\end{cases} \\ \dfrac{d+2}{4}\begin{cases}\dfrac{d+2}{8} \\ \dfrac{d+6}{8}\end{cases}\end{cases} \\ \dfrac{d+1}{2}\begin{cases}\dfrac{d+1}{4}\begin{cases}\dfrac{d+1}{8} \\ \dfrac{d+5}{8}\end{cases} \\ \dfrac{d+3}{4}\begin{cases}\dfrac{d+3}{8} \\ \dfrac{d+7}{8}\end{cases}\end{cases}\end{cases} 2d+0 4d+0 8d+08d+44d+2 8d+28d+62d+1 4d+1 8d+18d+54d+3 8d+38d+7.

显然第 n n n次接水后第一类水的比例的分母为 2 n − 1 2^{n-1} 2n1.

初始时 d = 0 d=0 d=0,即第一次接第二类水时,第二次显然应接第一类水,对应上表的下半部分,此时分母都为奇数.

初始时 d = 1 d=1 d=1,即第一次接第一类水时,第二次显然应接第二类水,对应上表的上半部分,此时分母都为奇数.

综上,配凑出的比例分子为奇数,分母为 2 n − 1 2^{n-1} 2n1,接水次数为 n n n.将 x y \dfrac{x}{y} yx化为最简分数后,检查 x x x y y y是否为奇数, x + y x+y x+y是否是 2 2 2的幂次即可.约分的正确性见C题证明.

代码 -> 2022CCPC湖北省赛-B(找规律)

void solve() {
	ll x, y, a, b; cin >> x >> y >> a >> b;

	ll d = gcd(x, y);
	x /= d, y /= d;
	ll sum = x + y;
	if ((x & 1) && (y & 1) && sum == lowbit(sum)) {
		for (int i = 63; i >= 0; i--) {
			if (sum >> i & 1) {
				cout << i + 1 << endl;
				return;
			}
		}
	}
	else cout << -1 << endl;
}

int main() {
	CaseT  // 单测时注释掉该行
	solve();
}


C. Potion(hard version)

题意 ( 3   s 3\ \mathrm{s} 3 s)

2022CCPC湖北省赛题解ABCFJKL_第3张图片

有一容量为 ( a + b ) (a+b) (a+b)且只有一个从下往上的刻度 a a a的量杯.有两类水,若能通过若干次接水倒水使得杯中两种水的比例为 x : y x:y x:y,输出最小接水次数;否则输出 − 1 -1 1.

t    ( 1 ≤ t ≤ 1 e 5 ) t\ \ (1\leq t\leq 1\mathrm{e}5) t  (1t1e5)组测试数据.每组测试数据输入四个整数 x , y , a , b    ( 1 ≤ a , b , x , y ≤ 1 e 18 ) x,y,a,b\ \ (1\leq a,b,x,y\leq 1\mathrm{e}18) x,y,a,b  (1a,b,x,y1e18).

思路

设接水次数为 ( n + 1 ) (n+1) (n+1),其中第一次为从刻度 0 0 0开始接满,其余为从刻度 a a a开始接满,显然这是最优的.

a b \dfrac{a}{b} ba x y \dfrac{x}{y} yx化为最简分数,其正确性后面证明.设 c = a + b c=a+b c=a+b.类似于B题的思路,易得第 n n n次接水后第一类水的比例的分母为 ( a + b ) n − 1 (a+b)^{n-1} (a+b)n1,则有解的必要条件是 ( a + b ) ∣ ( x + y ) (a+b)\mid (x+y) (a+b)(x+y).

逆向考虑该过程,若能配置出某种比例的水,将所有操作逆序后,最终能回到一类水的初始比例为 a a a,另一类水的比例为 b b b的初始状态,此时 a + b = x + y a+b=x+y a+b=x+y.此时再回退一步,得到只有一类水比例为 a a a的初始状态.

下面构造一个方案:设当前两种水的比例为 ( x , y ) (x,y) (x,y).

(1)若当前 x x x y y y都非零,不妨设 x ≤ y x\leq y xy;否则至少有一类水的比例为 0 0 0,此时找到一组解,输出答案即可.

​ ①若当前 ( a + b ) ∤ ( x + y ) (a+b)\not\mid(x+y) (a+b)(x+y),则无解.

​ ②若 ( a + b ) ∣ ( x + y ) (a+b)\mid (x+y) (a+b)(x+y),求出比例系数 k = x + y a + b ∈ Z k=\dfrac{x+y}{a+b}\in\mathbb{Z} k=a+bx+yZ,作为该步两类水的比例之和.

(2)①若 a ∤ x a\nmid x ax a ∤ y a\nmid y ay,则无解,因为最终无法使得一类水的比例为 a a a.

​ ②若 a ∣ x a\mid x ax,将 ( x a , k − x ) \left(\dfrac{x}{a},k-x\right) (ax,kx)作为新的 ( x , y ) (x,y) (x,y),回到(1).

​ ③若 a ∣ y a\mid y ay,将 ( y a , k − x ) \left(\dfrac{y}{a},k-x\right) (ay,kx)作为新的 ( x , y ) (x,y) (x,y),回到(1).

下面证明该方案的正确性:

[] 设有解,且最优解的答案为 a n s ans ans.将 a b \dfrac{a}{b} ba x y \dfrac{x}{y} yx都化为最简分数,由有解的必要条件: ( a + b ) ∣ ( x + y ) (a+b)\mid (x+y) (a+b)(x+y).

因第 n n n次接水后第一类水的比例的分母为 ( a + b ) n − 1 (a+b)^{n-1} (a+b)n1,则 a n s ≥ log ⁡ a + b ( x + y ) + 1 ans\geq \log_{a+b}(x+y)+1 ansloga+b(x+y)+1.

而上述方案给出了 a n s = log ⁡ a + b ( x + y ) + 1 ans=\log_{a+b}(x+y)+1 ans=loga+b(x+y)+1的构造,故它是最优解.

下面证明约分的正确性:

(1)设 a b \dfrac{a}{b} ba的最简分数为 a ′ b ′ \dfrac{a'}{b'} ba,则 a ′ b ′ \dfrac{a'}{b'} ba的若干整数倍相当于将量筒等比例地放大,显然这并不影响结果的步数,因为目标是凑出特定的比例,并无数量上的要求.

(2)若 x y \dfrac{x}{y} yx不是最简分数,将其化为最简分数会使步数减小.

[] 设 d = gcd ⁡ ( x , y ) ≥ 2 d=\gcd(x,y)\geq 2 d=gcd(x,y)2, x y \dfrac{x}{y} yx的最简分数为 x ′ y ′ \dfrac{x'}{y'} yx.

x y \dfrac{x}{y} yx的答案 a n s = log ⁡ a + b ( x + y ) + 1 = log ⁡ a + b ( d x ′ + d y ′ ) + 1 = log ⁡ a + b ( x ′ + y ′ ) + 1 + log ⁡ a + b d ans=\log_{a+b}(x+y)+1=\log_{a+b}(dx'+dy')+1=\log_{a+b}(x'+y')+1+\log_{a+b}d ans=loga+b(x+y)+1=loga+b(dx+dy)+1=loga+b(x+y)+1+loga+bd.

x ′ y ′ \dfrac{x'}{y'} yx的答案 a n s ′ = log ⁡ a + b ( x ′ + y ′ ) + 1 ans'=\log_{a+b}(x'+y')+1 ans=loga+b(x+y)+1,则 a n s = a n s ′ + log ⁡ a + b d ans=ans'+\log_{a+b}d ans=ans+loga+bd.因 d ≥ 2 > 1 d\geq 2>1 d2>1,故 log ⁡ a + b d > 0 \log_{a+b}d>0 loga+bd>0,故证.

代码 -> 2022CCPC湖北省赛-C(思维+构造)

void solve() {
	ll x, y, a, b; cin >> x >> y >> a >> b;

	ll d = gcd(x, y);
	x /= d, y /= d;  // 将x/y化为最简分数
	d = gcd(a, b);
	a /= d, b /= d;  // 将a/b化为最简分数

	ll ans = 0;
	while (x && y) {
		if (x > y) swap(x, y);  // 保证x小
    
    // cout << x << ' ' << y << endl;  // 输出方案

		if ((x + y) % (a + b)) {
			cout << -1 << endl;
			return;
		}

		ll k = (x + y) / (a + b);
		if (x % a == 0) {
			x = x / a;
			y = k - x;
			ans++;
		}
		else if (y % a == 0) {
			x = y / a;
			y = k - x;
			ans++;
		}
		else {
			cout << -1 << endl;
			return;
		}
	}
	cout << ans + 1 << endl;
}

int main() {
	CaseT  // 单测时注释掉该行
	solve();
}


L. Chtholly and the Broken Chronograph

题意

维护一个长度为 n n n的序列 a 1 , ⋯   , a n a_1,\cdots,a_n a1,,an,其中每个元素 a i a_i ai有一个状态 s i s_i si:① s i = 0 s_i=0 si=0表示第 i i i个元素禁用;② s i = 1 s_i=1 si=1表示第 i i i个元素启用.

现有如下四种操作:

1   x 1\ x 1 x,表示将 s x s_x sx置为 0 0 0,数据保证操作前 s x = 1 , 1 ≤ x ≤ n s_x=1,1\leq x\leq n sx=1,1xn

2   x 2\ x 2 x,表示将 s x s_x sx置为 1 1 1,数据保证操作前 s x = 0 , 1 ≤ x ≤ n s_x=0,1\leq x\leq n sx=0,1xn.

3   l   r   x 3\ l\ r\ x 3 l r x,表示将区间 [ l , r ] [l,r] [l,r]中所有 s i = 1 s_i=1 si=1 a i + = x a_i+=x ai+=x.数据保证 1 ≤ l ≤ r ≤ n , 1 ≤ x ≤ 1 e 8 1\leq l\leq r\leq n,1\leq x\leq 1\mathrm{e}8 1lrn,1x1e8.

4   l   r 4\ l\ r 4 l r,表示查询区间 [ l , r ] [l,r] [l,r]中所有 a i a_i ai之和,无论 a i a_i ai的状态.数据保证 1 ≤ l ≤ r ≤ n 1\leq l\leq r\leq n 1lrn.

第一行输入两个整数 n , q    ( 1 ≤ n , q ≤ 1 e 5 ) n,q\ \ (1\leq n,q\leq 1\mathrm{e}5) n,q  (1n,q1e5).第二行输入 n n n个整数 a 1 , ⋯   , a n    ( 1 ≤ a i ≤ 1 e 8 ) a_1,\cdots,a_n\ \ (1\leq a_i\leq 1\mathrm{e}8) a1,,an  (1ai1e8).第三行输入 n n n个整数 s 1 , ⋯   , s n    ( s i ∈ { 0 , 1 } ) s_1,\cdots,s_n\ \ (s_i\in\{0,1\}) s1,,sn  (si{0,1}).接下来 q q q行每行输入一个操作,格式如上.

思路

线段树节点维护一个变量 c n t cnt cnt表示区间内 s i = 1 s_i=1 si=1 a i a_i ai的个数.

代码 -> 2022CCPC湖北省赛-L(线段树)

const int MAXN = 1e5 + 5;
namespace SegmentTree {
	int n;
	int a[MAXN];
	int s[MAXN];  // 每个元素的状态
	struct Node {
		int l, r;
		int s;  // 元素的状态,s=0表示禁用,s=1表示启用
		int cnt;  // 区间s=1的元素个数
		ll sum;  // 区间和
		ll lazy;  // 加法懒标记
	}SegT[MAXN << 2];

	void push_up(int u) {
		SegT[u].sum = SegT[u << 1].sum + SegT[u << 1 | 1].sum;
		SegT[u].cnt = SegT[u << 1].cnt + SegT[u << 1 | 1].cnt;
	}

	void build(int u, int l, int r) {
		SegT[u].l = l, SegT[u].r = r;
		if (l == r) {
			SegT[u].sum = a[l];
			SegT[u].s = s[l];
			SegT[u].cnt = SegT[u].s;
			return;
		}

		int mid = l + r >> 1;
		build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
		push_up(u);
	}

	void push_down(int u) {
		SegT[u << 1].sum += SegT[u].lazy * SegT[u << 1].cnt;
		SegT[u << 1].lazy += SegT[u].lazy;
		SegT[u << 1 | 1].sum += SegT[u].lazy * SegT[u << 1 | 1].cnt;
		SegT[u << 1 | 1].lazy += SegT[u].lazy;
		SegT[u].lazy = 0;
	}

	void modify_state(int u, int x) {  // 反转a[x].s
		if (SegT[u].l == SegT[u].r) {  // 暴力修改叶子节点
			SegT[u].s ^= 1;
			SegT[u].cnt = SegT[u].s;
			return;
		}

		push_down(u);
		int mid = SegT[u].l + SegT[u].r >> 1;
		if (x <= mid) modify_state(u << 1, x);
		else modify_state(u << 1 | 1, x);
		push_up(u);
	}

	void modify_add(int u, int l, int r, int x) {
		if (l <= SegT[u].l && SegT[u].r <= r) {
			SegT[u].sum += (ll)SegT[u].cnt * x;
			SegT[u].lazy += x;
			return;
		}

		push_down(u);
		int mid = SegT[u].l + SegT[u].r >> 1;
		if (l <= mid) modify_add(u << 1, l, r, x);
		if (r > mid) modify_add(u << 1 | 1, l, r, x);
		push_up(u);
	}

	ll query(int u, int l, int r) {
		if (l <= SegT[u].l && SegT[u].r <= r) return SegT[u].sum;

		push_down(u);
		int mid = SegT[u].l + SegT[u].r >> 1;
		ll res = 0;
		if (l <= mid) res += query(u << 1, l, r);
		if (r > mid) res += query(u << 1 | 1, l, r);
		return res;
	}
};
using namespace SegmentTree;

void solve() {
	int q; cin >> n >> q;
	for (int i = 1; i <= n; i++) cin >> a[i];
	for (int i = 1; i <= n; i++) cin >> s[i];

	build(1, 1, n);

	while (q--) {
		int op; cin >> op;
		if (op == 1 || op == 2) {
			int x; cin >> x;
			modify_state(1, x);
		}
		else if (op == 3) {
			int l, r, x; cin >> l >> r >> x;
			modify_add(1, l, r, x);
		}
		else {
			int l, r; cin >> l >> r;
			cout << query(1, l, r) << endl;
		}
	}
}

int main() {
	solve();
}


F. Angel

题意

有编号 1 ∼ n 1\sim n 1n n n n个洞排成一行,兔子每次只能在一个洞中,且每个懂只能停留一秒.每一秒兔子会移动到相邻的洞,遇到边界时返回.每一秒兔子移动前,玩家可检查一个洞中是否有兔子,若有则抓到兔子.求一个最坏情况下能抓到兔子的所需时间最小的方案,或断言不能抓到兔子.

第一行输入一个整数 n    ( 1 ≤ n ≤ 1000 ) n\ \ (1\leq n\leq 1000) n  (1n1000).

若不能抓到兔子,输出 − 1 -1 1;否则第一行输出所需的最小时间,第二行输出每一秒检查的洞的编号.

思路

兔子移动后奇偶性改变,假定其初始位置的奇偶性后,将其往一边赶,最坏情况只需正着赶一遍再倒着赶一遍.

不妨设 n > 2 n>2 n>2.注意到初始在奇数位置和在偶数位置的兔子不会跳到同一格子,且每一时刻奇偶性互换.注意到每次检查一个洞实际上是排除了某类奇偶性的一个可能,若两类奇偶性都存在且存在空位时,检查一类的奇偶性会使得另一类的奇偶性增加一个可能性,故需先排除一种奇偶性的全部可能.消除一类奇偶性最少需 ( n − 2 ) (n-2) (n2)步,即从 2 , 3 , ⋯   , n − 1 2,3,\cdots,n-1 2,3,,n1,再将兔子倒着赶一遍,即 n − 1 , n − 2 , ⋯   , 2 n-1,n-2,\cdots,2 n1,n2,,2,即可保证抓到兔子.

注意特判 n = 1 n=1 n=1 n = 2 n=2 n=2的情况.

代码 -> 2022CCPC湖北省赛-F(思维)

void solve() {
	int n; cin >> n;

	if (n == 1) {
		cout << "1\n1" << endl;
		return;
	}
	else if (n == 2) {
		cout << "2\n2 2" << endl;
		return;
	}

	cout << 2 * (n - 2) << endl;
	for (int i = 2; i <= n - 1; i++) cout << i << ' ';
	for (int i = n - 1; i >= 2; i--) cout << i << ' ';
}

int main() {
	solve();
}


A. Nucleic Acid Test

题意

给定一个包含 n n n个节点和 m m m条边的无向图,其中 k    ( 1 ≤ k ≤ n ) k\ \ (1\leq k\leq n) k  (1kn)个节点处有核酸点.选择一个核酸点出发,以速度 v v v遍历所有节点,并最后在核酸点结束,要求相邻两次核酸间隔不超过时间 t t t,求满足要求的最小速度(整数).

第一行输入三个整数 n , m , k    ( 2 ≤ n ≤ 300 , 0 ≤ m ≤ n ( n − 1 ) 2 , 1 ≤ k ≤ n ) n,m,k\ \ \left(2\leq n\leq 300,0\leq m\leq \dfrac{n(n-1)}{2},1\leq k\leq n\right) n,m,k  (2n300,0m2n(n1),1kn).第二行输入一个整数 t    ( 0 ≤ t ≤ 1 e 9 ) t\ \ (0\leq t\leq 1\mathrm{e}9) t  (0t1e9).接下来 m m m行每行输入三个整数 a , b , c    ( 1 ≤ a , b ≤ n , 1 ≤ c ≤ 1 e 9 ) a,b,c\ \ (1\leq a,b\leq n,1\leq c\leq 1\mathrm{e}9) a,b,c  (1a,bn,1c1e9),表示节点 a a a b b b间存在长度为 c c c的无向边.数据保证无重边.最后一行输入 k k k个相异的整数 s 1 , ⋯   , s k s_1,\cdots,s_k s1,,sk,表示 k k k个核酸点所在的节点编号.

若能从一个核算点出发,遍历所有节点,并最后在核酸点结束,输出最小速度(整数);否则输出 − 1 -1 1.

思路I

n n n最大 300 300 300,显然可先用Floyd算法求出任意两点间的最短路.设非核酸点 x x x到核酸点 A A A B B B的距离分别为 d A d_A dA d B d_B dB,不妨设 d A > d B d_A>d_B dA>dB.若从 A A A出发经 x x x B B B,经过的路程为 d A + d B d_A+d_B dA+dB,造成的影响是 x x x被访问.注意到为访问 x x x,只需从 B B B出发,先走到 x x x,再返回 B B B,经过的路程为 2 d B < d A + d B 2d_B2dB<dA+dB,这表明:非核酸点对总路程的贡献是它到最近核酸点的距离的两倍.将核酸点和非核酸点分开考虑,则非核酸点与距其最近的核酸点可能将图分为若干个连通块,此时显然核酸点间通过最小生成树连通答案最优,且其对答案的影响取决于生成树中的最长边.

注意 t = 0 t=0 t=0或图不连通时无解.

代码I -> 2022CCPC湖北省赛-A(Floyd+Kruskal)

const int MAXN = 305, MAXM = MAXN * MAXN;
int n, m, k;  // 节点数、边数、核酸点数
int t;  // 最大核酸间隔
int nucleic[MAXN];  // 核酸点编号
bool is_nucleic[MAXN];  // 记录每个节点是否是核酸点

namespace Floyd {
	int n;  // 节点数
	ll d[MAXN][MAXN];  // d[u][v]表示节点u与v间的最短路

	void init() {  // 初始化d[][]
		for (int i = 1; i <= n; i++)
			for (int j = 1; j <= n; j++) d[i][j] = i == j ? 0 : INFF;
	}

	void floyd() {
		for (int k = 1; k <= n; k++) {
			for (int i = 1; i <= n; i++) 
				for (int j = 1; j <= n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
		}
	}
}

namespace Kruskal {
	int n, m;  // 节点数、边数
	struct Edge {
		int u, v;
		ll w;

		bool operator<(const Edge& B) { return w < B.w; }
	}edges[MAXM];
	int fa[MAXN];  // 并查集的fa[]数组

	void init() {  // 初始化fa[]
		for (int i = 1; i <= n; i++) fa[i] = i;
	}

	int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }

	ll kruskal() {  // 返回最小生成树的最长边,图不连通时返回INFF
		sort(edges, edges + m);  // 按边权升序排列

		ll res = 0;  // 最小生成树的最长边
		int cnt = 0;  // 当前连的边数
		for (int i = 0; i < m; i++) {
			auto [u, v, w] = edges[i];
			u = find(u), v = find(v);
			if (u != v) {
				fa[u] = v;
				res = max(res, w);
				cnt++;
			}
		}

		if (cnt < n - 1) return INFF;  // 图不连通
		else return res;
	}
}

void build() {  // 对核酸点建图
	Kruskal::m = 0;
	for (int i = 1; i <= k; i++) {
		for (int j = i + 1; j <= k; j++) 
			Kruskal::edges[Kruskal::m++] = { nucleic[i],nucleic[j],Floyd::d[nucleic[i]][nucleic[j]] };
	}
}

void solve() {
	cin >> n >> m >> k >> t;

	if (!t) {
		cout << -1;
		return;
	}

	Floyd::n = n;
	Floyd::init();

	while (m--) {
		int a, b, c; cin >> a >> b >> c;
		Floyd::d[a][b] = Floyd::d[b][a] = min(Floyd::d[a][b], (ll)c);
	}
	
	for (int i = 1; i <= k; i++) {
		cin >> nucleic[i];
		is_nucleic[nucleic[i]] = true;
	}

	Floyd::floyd();

	ll maxlength = 0;  // 每个节点到其最近的核酸点的最短距离的最大值
	for (int i = 1; i <= n; i++) {
		ll minlength = INFF;  // 每个节点到其最近的核酸点的最短距离
		if (is_nucleic[i]) {  // 核酸点
			for (int j = 1; j <= k; j++) {
				if (nucleic[j] == i) continue;

				minlength = min(minlength, Floyd::d[i][nucleic[j]]);
			}

			if (k == 1) minlength = 0;  // 特判只有一个核酸点的情况
		}
		else {  // 非核酸点
			for (int j = 1; j <= k; j++)  // 找到距该非核酸点最近的核酸点
				minlength = min(minlength, Floyd::d[i][nucleic[j]] << 1);
		}
		maxlength = max(maxlength, minlength);
	}

	Kruskal::n = n;
	Kruskal::init();  // 注意并查集要对n个节点都初始化
	Kruskal::n = k;  // 实际图中只有k个节点
	build();
	maxlength = max(maxlength, Kruskal::kruskal());

	if (maxlength == INFF) cout << -1;
	else cout << (maxlength + t - 1) / t;
}

int main() {
	solve();
}

在这里插入图片描述


思路II

二分速度 v v v,每次只对距离不超过 v t vt vt的两节点连边,检查图的连通性即可.

代码II -> 2022CCPC湖北省赛-A(Floyd+并查集+二分)

const int MAXN = 305, MAXM = MAXN * MAXN;
int n, m, k;  // 节点数、边数、核酸点数
int t;  // 最大核酸间隔
bool is_nucleic[MAXN];  // 记录每个节点是否是核酸点

namespace Floyd {
	int n;  // 节点数
	ll d[MAXN][MAXN];  // d[u][v]表示节点u与v间的最短路

	void init() {  // 初始化d[][]
		for (int i = 1; i <= n; i++)
			for (int j = 1; j <= n; j++) d[i][j] = i == j ? 0 : INFF;
	}

	void floyd() {
		for (int k = 1; k <= n; k++) {
			for (int i = 1; i <= n; i++) 
				for (int j = 1; j <= n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
		}
	}
}

namespace DSU {
	int n;  // 元素个数
	int fa[MAXN];  // fa[]数组
	int siz[MAXN];  // 集合大小

	void init() {  // 初始化fa[]、siz[]
		for (int i = 1; i <= n; i++) fa[i] = i, siz[i] = 1;
	}

	int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }

	bool merge(int x, int y) {  // 返回合并是否成功,即初始时是否不在同一集合中
		x = find(x), y = find(y);
		if (x != y) {
			if (siz[x] > siz[y]) swap(x, y);  // 保证x所在的集合小

			fa[x] = y;
			siz[y] += siz[x];
			return true;
		}
		else return false;
	}
}

bool vis[MAXN];  // 记录每个集合是否被遍历过

bool check(ll v) {
	DSU::init();
	for (int i = 1; i <= n; i++) vis[i] = false;

	// 核酸点间连边
	for (int i = 1; i <= n; i++) {
		if (!is_nucleic[i]) continue;

		for (int j = 1; j <= n; j++) {
			if (!is_nucleic[j]) continue;

			// if (v * t >= Floyd::d[i][j]) DSU::merge(i, j);  // 注意此处乘法会爆ll
			if ((double)Floyd::d[i][j] / v <= (double)t) DSU::merge(i, j);
		}
	}

	// 非核酸点向核酸点连边
	for (int i = 1; i <= n; i++) {
		if (is_nucleic[i]) continue;

		for (int j = 1; j <= n; j++) {
			if (!is_nucleic[j]) continue;

			// if (v * t >= 2 * Floyd::d[i][j]) DSU::merge(i, j);  // 注意此处乘法会爆ll
			if ((double)2 * Floyd::d[i][j] / v <= (double)t) DSU::merge(i, j);
		}
	}
	
	int cnt = 0;  // 连通块个数
	for (int i = 1; i <= n; i++) {
		int j = DSU::find(i);
		if (!vis[j]) {
			cnt++;
			vis[j] = true;

			if (cnt >= 2) return false;  // 图不连通
		}
	}
	return true;
}

void solve() {
	cin >> n >> m >> k >> t;

	if (!t) {
		cout << -1;
		return;
	}

	Floyd::n = n;
	Floyd::init();

	while (m--) {
		int a, b, c; cin >> a >> b >> c;
		Floyd::d[a][b] = Floyd::d[b][a] = min(Floyd::d[a][b], (ll)c);
	}
	
	for (int i = 1; i <= k; i++) {
		int x; cin >> x;
		is_nucleic[x] = true;
	}

	Floyd::floyd();

	// 检查图的连通性
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++) {
			if (Floyd::d[i][j] >= INFF) {  // 图不连通
				cout << -1;
				return;
			}
		}
	}
	
	DSU::n = n;

	ll l = 1, r = 1e18;  // 注意l最小取1
	while (l < r) {
		ll mid = l + r >> 1;
		if (check(mid)) r = mid;
		else l = mid + 1;
	}
	cout << l;
}

int main() {
	solve();
}

在这里插入图片描述



J. Palindrome Reversion

题意

给定一个长度不超过 1 e 5 1\mathrm{e}5 1e5且只包含小写英文字母的字符串 s s s,下标从 1 1 1开始.问是否能翻转一个区间使得 s s s变为回文串,若能,输出翻转的区间;否则输出 − 1   − 1 -1\ -1 1 1.若有多组解,输出任一组.

思路

显然初始时 s s s首尾对称的部分不影响是否有解,可将其去掉.

观察样例,猜测若有解,则可通过翻转 s s s的前缀或后缀使其变为回文串.

有解的充要条件是:可通过翻转 s s s的前缀或后缀使其变为回文串.

在这里插入图片描述

[] 如上图,用 s 1 s_1 s1的符号表示回文串.显然有解的情况如 s 2 s_2 s2 s 3 s_3 s3所示.

分别翻转 s 2 s_2 s2 s 3 s_3 s3的红色(后缀)、蓝色(前缀)部分即可将其变为回文串.

反之,通过翻转 s 1 s_1 s1的某一部分可将其变为有解的情况 s 2 s_2 s2 s 3 s_3 s3.

代码 -> 2022CCPC湖北省赛-J(字符串哈希)

const int MAXN = 1e6 + 5;

struct StringHash {
	int n;  // 字符串长度
	char str[MAXN];  // 下标从1开始
	const ll Base1 = 29, MOD1 = 1e9 + 7;
	const ll Base2 = 131, MOD2 = 1e9 + 9;
	ll ha1[MAXN], ha2[MAXN];  // 正着的哈希值
	ll rha1[MAXN], rha2[MAXN];  // 反着的哈希值
	ll pow1[MAXN], pow2[MAXN];  // Base1和Base2的乘方

	void init() {  // 预处理pow1[]、pow2[]
		pow1[0] = pow2[0] = 1;
		for (int i = 1; i <= n; i++) {
			pow1[i] = pow1[i - 1] * Base1 % MOD1;
			pow2[i] = pow2[i - 1] * Base2 % MOD2;
		}
	}

	void pre() {  // 预处理ha1[]、ha2[]
		for (int i = 1; i <= n; i++) {
			ha1[i] = (ha1[i - 1] * Base1 + str[i]) % MOD1;
			ha2[i] = (ha2[i - 1] * Base2 + str[i]) % MOD2;
			rha1[i] = (rha1[i - 1] * Base1 + str[n - i + 1]) % MOD1;
			rha2[i] = (rha2[i - 1] * Base2 + str[n - i + 1]) % MOD2;
		}
	}

	pll get_hash(int l, int r) {  // 求子串str[l...r]正着的哈希值
		ll res1 = ((ha1[r] - ha1[l - 1] * pow1[r - l + 1]) % MOD1 + MOD1) % MOD1;
		ll res2 = ((ha2[r] - ha2[l - 1] * pow2[r - l + 1]) % MOD2 + MOD2) % MOD2;
		return pll(res1, res2);
	}

	pll get_rhash(int l, int r) {  // 求子串str[l...r]反着的哈希值
		ll res1 = ((rha1[n - l + 1] - rha1[n - r] * pow1[r - l + 1]) % MOD1 + MOD1) % MOD1;
		ll res2 = ((rha2[n - l + 1] - rha2[n - r] * pow2[r - l + 1]) % MOD2 + MOD2) % MOD2;
		return pll(res1, res2);
	}

	bool IsPalindrome(int l, int r) {  // 判断子串str[l...r]是否是回文串
		return get_hash(l, r) == get_rhash(l, r);
	}

	pll add(pll a, pll b) {
		ll res1 = (a.first + b.first) % MOD1;
		ll res2 = (a.second + b.second) % MOD2;
		return pll(res1, res2);
	}

	pll mul(pll& a, ll k) {  // a *= Base的k次方
		ll res1 = a.first * pow1[k] % MOD1;
		ll res2 = a.second * pow2[k] % MOD2;
		return pll(res1, res2);
	}
}solver;

void solve() {
	cin >> solver.str + 1;

	solver.n = strlen(solver.str + 1);
	solver.init();
	solver.pre();

	int l = 1, r = solver.n;
	while (l < r && solver.str[l] == solver.str[r]) l++, r--;  // 去掉两边相同的字符

	if (l >= r) {  // 原串是回文串
		cout << "1 1" << endl;
		return;
	}

	for (int i = l; i <= r; i++) {  // 翻转前缀str[l...i]
		auto tmp1 = solver.get_rhash(l, i), tmp2 = solver.get_hash(i + 1, r);
		auto res1 = solver.add(solver.mul(tmp1, r - i), tmp2);
		auto tmp3 = solver.get_hash(l, i), tmp4 = solver.get_rhash(i + 1, r);
		auto res2 = solver.add(solver.mul(tmp4, i - l + 1), tmp3);
		if (res1 == res2) {
			cout << l << ' ' << i;
			return;
		}
	}

	for (int i = l; i <= r; i++) {  // 反转后缀str[i...r]
		auto tmp1 = solver.get_hash(l, i - 1), tmp2 = solver.get_rhash(i, r);
		auto res1 = solver.add(solver.mul(tmp1, r - i + 1), tmp2);
		auto tmp3 = solver.get_rhash(l, i - 1), tmp4 = solver.get_hash(i, r);
		auto res2 = solver.add(solver.mul(tmp4, i - l), tmp3);
		if (res1 == res2) {
			cout << i << ' ' << r;
			return;
		}
	}

	cout << "-1 -1";
}

int main() {
	solve();
}


你可能感兴趣的:(CCPC,ICPC补题,算法)