2021CCPC山东省赛题解BCDFGHM

本文介绍了2021年CCPC山东省赛中的四个题目,包括GPA成绩计算(涉及整数除法和取模运算)、冒险者任务收益(动态规划求解)、方块重力场图形周长(空间复杂度优化)和矩阵问题构造(逻辑与构造算法)。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2021CCPC山东省赛题解BCDFGHM

G. Grade Point Average

题意 (2 s2\ \mathrm{s}2 s)

给定n  (1≤n≤1e5)n\ \ (1\leq n\leq 1\mathrm{e}5)n  (1n1e5)门课的成绩(整数)a1,⋯ ,an  (0≤ai≤100)a_1,\cdots,a_n\ \ (0\leq a_i\leq 100)a1,,an  (0ai100),求GPA,下取整保留到小数点后k  (1≤k≤1e5)k\ \ (1\leq k\leq 1\mathrm{e}5)k  (1k1e5)位.

思路

显然当且仅当a=ba=ba=bab\dfrac{a}{b}ba的整数部分为111,其余情况整数部分为000.

注意到ab\dfrac{a}{b}ba小数点后第111位即10ab\dfrac{10a}{b}b10a的整数部分,但kkk最大为1e51\mathrm{e}51e5,乘1e51\mathrm{e}51e5次会爆掉.注意到对答案有贡献的只有aaabbb的余数,故每次a∗=10a*=10a=10,计算答案后a%=ba\%=ba%=b即可.

代码 -> 2021CCPC山东省赛-G(思维+模拟)

void solve() {
	int n, k; cin >> n >> k;
	ll sum = 0;
	for (int i = 0; i < n; i++) {
		int a; cin >> a;
		sum += a;
	}

	cout << sum / n << '.';
	sum %= n;
	for (int i = 0; i < k; i++) {
		sum *= 10;
		cout << sum / n;
		sum %= n;
	}
}

int main() {
	solve();
}


H. Adventurer’s Guild

题意 (2 s2\ \mathrm{s}2 s)

有编号1∼n1\sim n1nnnn个任务,其中第i  (1≤i≤n)i\ \ (1\leq i\leq n)i  (1in)个任务消耗hih_ihi点生命和sis_isi点耐力,可获得wiw_iwi的金币.初始时生命为HHH,耐力为SSS.当玩家生命≤0\leq 00时死亡.玩家耐力<0<0<0时,可用生命填补耐力的透支,如玩家耐力为−3-33且生命要减333时,耐力会被置为000,若玩家的生命不足以填补耐力的透支,玩家死亡.问玩家不死亡的前提下最多能获得多少金币.

第一行输入三个整数n,H,S  (1≤n≤1000,1≤H≤300,0≤S≤300)n,H,S\ \ (1\leq n\leq 1000,1\leq H\leq 300,0\leq S\leq 300)n,H,S  (1n1000,1H300,0S300).接下来nnn行每行输入三个整数h,s,w  (0≤h,s≤300,1≤w≤1e9)h,s,w\ \ (0\leq h,s\leq 300,1\leq w\leq 1\mathrm{e}9)h,s,w  (0h,s300,1w1e9).

思路

dp[i][j][k]dp[i][j][k]dp[i][j][k]表示只考虑前iii个任务、当前血量为jjj、耐力为kkk时的最大收益,压第一维,最终答案为dp[H][S]dp[H][S]dp[H][S].

状态转移方程:dp[j][k]=max⁡1≤i≤ndp[j−h[i]+min⁡{0,k−s[i]}][max⁡{0,k−s[i]}]+w[i]\displaystyle dp[j][k]=\max_{1\leq i\leq n}dp[j-h[i]+\min\{0,k-s[i]\}][\max\{0,k-s[i]\}]+w[i]dp[j][k]=1inmaxdp[jh[i]+min{0,ks[i]}][max{0,ks[i]}]+w[i].

代码 -> 2021CCPC山东省赛-H(二维费用背包)

const int MAXN = 1005;
int n, H, S;
int h[MAXN], s[MAXN], w[MAXN];  // 消耗生命、消耗耐力、获得收益
ll dp[MAXN][MAXN];  // dp[i][j][k]表示只考虑前i个任务、当前血量为j、耐力为k时的最大收益

void solve() {
	cin >> n >> H >> S;
	for (int i = 1; i <= n; i++) cin >> h[i] >> s[i] >> w[i];

	for (int i = 1; i <= n; i++) {  // 枚举任务
		for (int j = H; j; j--) {  // 枚举生命,注意j≥1
			for (int k = S; k >= 0; k--) {  // 枚举耐力
				if (j > h[i] && j + k > h[i] + s[i])
					dp[j][k] = max(dp[j][k], dp[j - h[i] + min(0, k - s[i])][max(0, k - s[i])] + w[i]);
			}
		}
	}
	cout << dp[H][S];
}

int main() {
	solve();
}


D. Dyson Box

题意 (2 s2\ \mathrm{s}2 s)

某盒子中重力场的方向在打开它之前不确定,盒子内部可视为一个二维网格,左下角的坐标为(0,0)(0,0)(0,0),右上角的坐标为(2e5,2e5)(2\mathrm{e}5,2\mathrm{e}5)(2e5,2e5).现有nnn个事件,其中第i  (1≤i≤n)i\ \ (1\leq i\leq n)i  (1in)个事件(xi,yi)(x_i,y_i)(xi,yi)表示在盒子中添加一个方块,其左下角坐标为(xi−1,yi−1)(x_i-1,y_i-1)(xi1,yi1),右上角坐标为(xi,yi)(x_i,y_i)(xi,yi).每个盒子中重力场的方向有12\dfrac{1}{2}21的概率为水平的,有12\dfrac{1}{2}21的概率为竖直的,水平的重力场会将所有方块推到盒子的左边,竖直的重力场会将所有方块推到盒子的下边.问每次打开一个盒子后所有方块构成的图形的周长.

第一行输入一个整数n  (1≤n≤2e5)n\ \ (1\leq n\leq 2\mathrm{e}5)n  (1n2e5),表示事件数.接下来nnn行每行输入两个整数x,y  (1≤x,y≤2e5)x,y\ \ (1\leq x,y\leq 2\mathrm{e}5)x,y  (1x,y2e5),表示方块的坐标.数据保证任意两个方块坐标相异.

对每个事件输出两个整数,分别表示该盒子中重力场方向是竖直、水平时所有方块构成的图形的周长.

思路

以竖直的重力场为例,水平同理.

(1)若新方块(红色,下同)所在列无其他方块,则ans+=2ans+=2ans+=2,即方块上下两条边的长度.

在这里插入图片描述

(2)若新方块所在列的左边有方块(蓝色,下同),且原有的方块的高度之和不超过新方块的高度,则ans++ans++ans++,即新方块右边的长度.

在这里插入图片描述

(3)若新方块所在列的左边有方块,且原有的方块高度之和超过新方块的高度,则ans−−ans--ans,即新方块左边的长度.

在这里插入图片描述

(4)新方块所在列的右边有方块的情况同理.

在这里插入图片描述

注意答案可能爆int.

代码 -> 2021CCPC山东省赛-D(思维)

const int MAXN = 2e5 + 5;
int hor[MAXN], ver[MAXN];  // 水平、竖直方向每个坐标的方块数

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

	ll anshor = 0, ansver = 0;
	while (n--) {
		int x, y; cin >> x >> y;

		if (!hor[x]) anshor += 2;
		if (hor[x - 1] <= hor[x]) anshor++;
		else anshor--;
		if (hor[x + 1] <= hor[x]) anshor++;
		else anshor--;
		hor[x]++;

		if (!ver[y]) ansver += 2;
		if (ver[y - 1] <= ver[y]) ansver++;
		else ansver--;
		if (ver[y + 1] <= ver[y]) ansver++;
		else ansver--;
		ver[y]++;

		cout << anshor << ' ' << ansver << endl;
	}
}

int main() {
	solve();
}


M. Matrix Problem

题意 (2 s2\ \mathrm{s}2 s)

给定两n×mn\times mn×m0−10-101矩阵AAABBB,两个矩阵中的111各自连通(四连通),由它们得到一个n×mn\times mn×m0−10-101矩阵CCC,使得CCC中的元素为111当且仅当AAABBB的对应位置元素都为111;否则CCC中的元素为000.现给定n×m  (3≤n,m≤500)n\times m\ \ (3\leq n,m\leq 500)n×m  (3n,m500)0−10-101矩阵CCC,构造两个满足上述要求的0−10-101矩阵AAABBB,若有多组解,输出任一组.数据保证CCC的外边界元素都为000.

思路

以如下图所示的矩阵为框架即可,其中红色、蓝色方格分别表示矩阵AAABBB111的位置

在这里插入图片描述

先将矩阵AAABBB中矩阵CCC中为111的位置置111,对矩阵CCC中为000的位置保证AAABBB相反即可.

代码 -> 2021CCPC山东省赛-M(构造)

const int MAXN = 505;
int n, m;
int a[MAXN][MAXN], b[MAXN][MAXN], c[MAXN][MAXN];

void solve() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m; j++) {
			char ch; cin >> ch;
			c[i][j] = ch - '0';
			if (c[i][j]) a[i][j] = b[i][j] = 1;
		}
	}

	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m - 1; j++) 
			if (j == 1 || i & 1) a[i][j] = 1;
	}

	for (int i = 1; i <= n; i++) {
		for (int j = m; j >= 2; j--)
			if (j == m || (i % 2 == 0 && !a[i][j])) b[i][j] = 1;
	}

	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m; j++) cout << a[i][j];
		cout << endl;
	}
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m; j++) cout << b[i][j];
		cout << endl;
	}
}

int main() {
	solve();
}


C. Cat Virus

题意 (2 s2\ \mathrm{s}2 s)

给一棵有根树的节点染黑色或白色,要求若一个节点染成黑色,则以它为根节点的子树中的节点也要染成黑色.给定整数k  (2≤k≤2e18)k\ \ (2\leq k\leq 2\mathrm{e}18)k  (2k2e18),构造一棵以节点111为根节点的有根树,使得它的染色方案恰有kkk种,先输出节点数n  (1≤n≤1e5)n\ \ (1\leq n\leq 1\mathrm{e}5)n  (1n1e5),再输出它的(n−1)(n-1)(n1)条边.数据保证有解.若有多组解,输出任一组.

思路

设节点uuunnn个子节点v1,⋯ ,vnv_1,\cdots,v_nv1,,vn.若uuu染黑色,则所有vi  (1≤i≤n)v_i\ \ (1\leq i\leq n)vi  (1in)只能染黑色,有111种方案;若uuu染白色,则所有vi  (1≤i≤n)v_i\ \ (1\leq i\leq n)vi  (1in)可染黑色或白色,有2n2^n2n种方案,故每个这样的单元的方案数为2n+12^n+12n+1.dp[u]dp[u]dp[u]表示以uuu为根节点的子树的方案数,显然dp[u]=∏v∈sonudp[v]+1\displaystyle dp[u]=\prod_{v\in son_u}dp[v]+1dp[u]=vsonudp[v]+1,则最后整棵树染色的方案数有形式k=(2n1+1)(2n2+1)⋯k=(2^{n_1}+1)(2^{n_2}+1)\cdotsk=(2n1+1)(2n2+1).

222的幂次想到构造二叉树.考察如下四种单元的方案数(每个单元最上方的节点为子树的根节点):

在这里插入图片描述

(1)孤立的节点,方案数为222.

(2)两个节点,方案数为333.

(3)三个节点成链,方案数为444.

(4)三个节点成树,方案数为555.

综上,特判方案数为222333的情况,二叉树不分叉的染色方案数为偶数,二叉树分叉的染色方案数为奇数,据此构造即可.

代码I -> 2021CCPC山东省赛-C(构造) By : 莫说啥

void solve() {
	ll k; cin >> k;

	vii ans;
	int n = 1;  // 当前用到的节点编号
	while (k) {
		if (k == 2) break;  // 特判
		if (k == 3) {  // 特判
			ans.push_back({ n,n + 1 });
			n++;
			break;
		}

		if (k & 1) {
			ans.push_back({ n,n + 1 }), ans.push_back({ n,n + 2 });  // 分叉
			n += 2;
			k >>= 1;
		}
		else {
			ans.push_back({ n,n + 1 });  // 不分叉
			n++;
			k--;
		}
	}

	cout << n << endl;
	for (auto& [u, v] : ans) cout << u << ' ' << v << endl;
}

int main() {
	solve();
}

代码II -> 2021CCPC山东省赛-C(构造+DFS) By : LZVSDY

const int MAXN = 1e5 + 5;
ll k;
int n = 1;  // 当前用到的节点编号
vi edges[MAXN];

void dfs(int u, ll cur) {  // 当前节点、所需方案数
	ll tmp = cur - 1;  // 注意-1'
	while (tmp % 2 == 0) {
		edges[u].push_back(++n);
		tmp >>= 1;
	}
	
	if (tmp == 1) return;  // 只差全部染黑的情况

	edges[u].push_back(++n);
	dfs(n, tmp);
}

void solve() {
	cin >> k;

	dfs(1, k);
	cout << n << endl;
	for (int u = 1; u <= n; u++)
		for (auto v : edges[u]) cout << u << ' ' << v << endl;
}

int main() {
	solve();
}


B. Build Roads

题意 (2 s2\ \mathrm{s}2 s)

有编号1∼n1\sim n1nnnn个城市,城市i  (1≤i≤n)i\ \ (1\leq i\leq n)i  (1in)处有一个经验值为aia_iai的施工队.现要建造(n−1)(n-1)(n1)条路连接这些城市,连接城市iii与城市jjj时的花费为gcd⁡(ai,aj)\gcd(a_i,a_j)gcd(ai,aj).求连接nnn个城市的最小代价.

a1,⋯ ,ana_1,\cdots,a_na1,,an由如下代码产生:

在这里插入图片描述

第一行输入四个整数n,L,R,seed  (2≤n≤2e5,1≤L≤R≤2e5,1≤seed≤1e18)n,L,R,seed\ \ (2\leq n\leq 2\mathrm{e}5,1\leq L\leq R\leq 2\mathrm{e}5,1\leq seed\leq 1\mathrm{e}18)n,L,R,seed  (2n2e5,1LR2e5,1seed1e18).

思路

显然MST,但节点数最大为2e52\mathrm{e}52e5,用Prim算法会TLE;图是nnn阶无向完全图,边数为n(n−1)2\dfrac{n(n-1)}{2}2n(n1),最大约2e102\mathrm{e}102e10,用Kruskal算法会TLE.考察Kruskal算法最多能跑几阶无向完全图.令mlog⁡m=1e8m\log m=1\mathrm{e}8mlogm=1e8,解得m=eW(1e8)≈6.4e6m=\mathrm{e}^{W(1\mathrm{e}8)}\approx 6.4\mathrm{e}6m=eW(1e8)6.4e6.令n(n−1)2=6.4e6\dfrac{n(n-1)}{2}=6.4\mathrm{e}62n(n1)=6.4e6,解得n≈3758n\approx 3758n3758,此时求出各边边权的时间复杂度为n(n+1)2≈7e6\dfrac{n(n+1)}{2}\approx 7\mathrm{e}62n(n+1)7e6.故n≤3758n\leq 3758n3758的情况可用Kruskal算法求解.

考察n>3758n>3758n>3758的情况:

(1)L=RL=RL=R时,显然答案为(n−1)L(n-1)L(n1)L.

*以下的解释不严谨,甚至可能错误,但代码可以AC.

(2)L≠RL\neq RL=R时,答案为(n−1)(n-1)(n1),因为nnn较大时,元素间的n(n−1)2\dfrac{n(n-1)}{2}2n(n1)gcd⁡\gcdgcd中至少有(n−1)(n-1)(n1)个为111,

​ 而数组a[]a[]a[]随机,可认为这(n−1)(n-1)(n1)111即构成MST的边的边权.

​ 可以证明这样发生错误的概率足够小.

代码 -> 2021CCPC山东省赛-B(思维+Kruskal)

const int MAXN = 2e5 + 5, MAXM = MAXN << 2;
int L, R;
ull seed;
int a[MAXN];

ull xorshift64() {
	ull x = seed;
	x ^= x << 13, x ^= x >> 7, x ^= x << 17;
	return seed = x;
}

int gen() { return xorshift64() % (R - L + 1) + L; }

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 += w;
				cnt++;
			}
		}

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

void solve() {
	cin >> n >> L >> R >> seed;
	for (int i = 1; i <= n; i++) a[i] = gen();

	if (L == R) {
		cout << (ll)L * (n - 1);
		return;
	}

	if (n > 3758) {
		cout << n - 1;
		return;
	}

	for (int i = 1; i <= n; i++) {
		for (int j = i + 1; j <= n; j++)
			edges[m++] = { i,j,gcd(a[i], a[j]) }, edges[m++] = { j,i,gcd(a[i], a[j]) };
	}
	
	init();
	cout << kruskal();
}

int main() {
	solve();
}


F. Birthday Cake

题意

给定n  (1≤n≤4e5)n\ \ (1\leq n\leq 4\mathrm{e}5)n  (1n4e5)个长度不超过4e54\mathrm{e}54e5的字符串,问其中有多少对不同的字符串使得将两个字符串首尾相连并从中间切开后可得到两个相同的字符串.数据保证所有字符串的长度之和不超过4e54\mathrm{e}54e5.

思路

显然两个相同的字符串符合要求.

在这里插入图片描述

对不同的字符串,显然符合要求的只有上图中的情况,其中蓝色部分为串的公共前后缀,该长度的公共前后缀对答案的贡献即红色部分出现的次数.

用map对哈希值计数即可.

代码 -> 2021CCPC山东省赛-F(字符串哈希+map)

const int MAXN = 4e5 + 5;

namespace 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);
	}
};
using namespace StringHash;

map<pll, int> cnt1, cnt2;  // 整个字符串出现的次数、字符串在其他字符串的内部出现的次数

void solve() {
	n = MAXN - 1;
	init();

	ll ans = 0;
	CaseT{
		cin >> str + 1;
		n = strlen(str + 1);
		pre();

		auto tmp = get_hash(1, n);
		ans += cnt1[tmp]++;  // 相同串出现的次数对答案的贡献,注意++的顺序
		ans += cnt2[tmp];  // 该字符串在其他串的内部对答案的贡献

		for (int len = 1; len * 2 <= n; len++) {  // 枚举公共前后缀长度
			auto pre = get_hash(1, len), suf = get_hash(n - len + 1, n);
			if (pre == suf) {
				auto mid = get_hash(len + 1, n - len);  // 中间部分
				ans += cnt1[mid];  // 与中间部分相同的串对答案的贡献
				cnt2[mid]++;  // 更新该字符串在其他串的内部出现的次数
			}
		}
	}
	cout << ans;
}

int main() {
	solve();
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值