牛客小白月赛45 题解

A题 悬崖 (脑筋急转弯)

脑筋急转弯题,就不翻译题目了,没必要。

nx 时,可以一直跳到两个墙合并,答案为 nx

n>x 时,至多跳一次就掉下去,但是还是有跳跃距离的,答案为 x

#include<bits/stdc++.h> using namespace std; int main() { long long x, n, ans; cin >> x >> n; ans = n > x ? x : n * x; cout << ans << endl; return 0; }

B题 数数 (签到)

给定一个函数:

void dfs(int cnt) { //cnt从1开始 如同dfs(1) for(int i = 1;i <= cnt;i++) ans++; dfs(cnt + 2); }

问函数递归到第 n 层时,ans 的值为多少?(从 dfs(1) 开始,ans 是全局变量,初始值为 0)

样例:n=2 时,ans=4

0n109

不难发现,第 i 层会让答案增加 2i1,所以 ans=i=1n2i1=n2

#include<bits/stdc++.h> using namespace std; int main() { long long n; cin >> n; cout << n * n; return 0; }

C题 山楂 (数学,贪心)

已知现在有不少等级不同的糖果,我们可以每次选择将 3 或 4 个 i 级的糖果合成为一个 i+1 级的糖果,并获得 xi 点积分。当若干个 8 级糖果被合成为一个 9 级糖果后,这个糖果会消失,不再能够被继续合成。

现在给定前八级糖果的数量 a1,a2,,a8,问我们能够合成的最高积分是多少?

0ai109

糖果只能从低级向高级合成,所以我们贪心的考虑,先低级后高级。

我们假设 k 级糖果的数量为 n,那么 n<3 时无法合成,n=3 时可以将三个糖果合并,n=4,5 时则将四个全部合并。

n6 时候,有一个不是很显然的性质:n 一定能够被表示为 3x+4y(x,y0) 的形式(可以同余证明,或者去参考 小凯的疑惑),这意味着这一级的糖果必然能被全部合成,即给积分贡献 nk。那么,我们必然要最大化合成的 k+1 级糖果数量,显然数量为 n3

#include<bits/stdc++.h> using namespace std; #define LL long long LL a[10]; const LL v[6] = {0, 0, 0, 3, 4, 4}; int main() { for (int i = 1; i <= 8; ++i) cin >> a[i]; LL ans = 0; for (int k = 1; k <= 8; ++k) { LL n = a[k]; ans += k * (n > 5 ? n : v[n]); a[k + 1] += n / 3; } cout << ans; return 0; }

D题 切糕 (思维,前缀和)

给定一个括号串,可以可以将其切上几刀或者不切,将其变为若干个合法括号串,问有多少种切的方式?

合法括号串:串内左右括号数量相等,且任意前缀内左括号数量不少于右括号。

|s|106,答案对 109+7 取模

若干个合法括号串的拼接必然也是一个合法括号串,所以我们直接对原括号串扫一遍,看看合不合法。

如果合法,我们重新开始,每找到能切的地方就标记一下,最后记标记的总数量为 cnt,那么最后答案就是 2cnt

#include<bits/stdc++.h> using namespace std; #define LL long long const int N = 1000010; const LL mod = 1e9 + 7; int n = 0, a[N], sum[N]; int main() { //read string s; cin >> s; for (char c : s) a[++n] = c == '(' ? 1 : -1; //solve bool NoSolution = false; int cnt = 0; for (int i = 1; i <= n; ++i) { sum[i] = sum[i - 1] + a[i]; if (sum[i] == 0 && i != n) cnt++; if (sum[i] < 0) NoSolution = true; } if (sum[n] != 0) NoSolution = true; if (NoSolution) { cout << -1; return 0; } LL ans = 1; for (int i = 0; i < cnt; ++i) ans = ans * 2 % mod; cout << ans; return 0; }

E题 筑巢 (树形DP)

给定一棵树,树上的点和边都拥有一个舒适值

现在想要在树上选择一个连通块,使得连通块内部的点和边的舒适值之和最大。

n105,|w|109

我们考虑一手树形 DP,从根节点 1 开始,那么显然,这个连通块要么包含自己,要么在它的某一棵子树中。

那么,我们尝试设立状态 dpx,0/1,表示点 x 在选或者不选下的所得连通块最大值,那么有 DP 方程:

dpx,0=maxysun(x)max(dpy,0,dpy,1)dpx,1=wx+(y,v)sun(x)max(0,dpy,1)

#include<bits/stdc++.h> using namespace std; #define LL long long const int N = 100010; int n, w[N]; struct Edge { int to, val; }; vector<Edge> tree[N]; LL dp[N][2]; void dfs(int x, int fa) { dp[x][0] = -1e18, dp[x][1] = w[x]; for (Edge e : tree[x]) { int y = e.to, v = e.val; if (y != fa) { dfs(y, x); dp[x][0] = max(dp[x][0], max(dp[y][0], dp[y][1])); dp[x][1] = dp[x][1] + max(dp[y][1] + v, 0LL); } } } int main() { cin >> n; for (int i = 1; i <= n; ++i) cin >> w[i]; for (int i = 1; i < n; ++i) { int x, y, v; cin >> x >> y >> v; tree[x].push_back((Edge){y, v}); tree[y].push_back((Edge){x, v}); } dfs(1, 0); cout << max(dp[1][0], dp[1][1]); return 0; }

F题 交换

题面很详细且简洁,就不翻译了。

我们记排列长度 p=10

对于每次询问,我们考虑枚举所有可能的区间并进行操作,总复杂度为 O(mn3p)

我们参考这场的前缀和训练题 牛牛的猜球游戏,来对区间操作进行优化,可以将复杂度降到 [mn2p]。不过显然,枚举的复杂度降不了了,必须得进行优化。

(我一开始以为这个答案长度可以二分,等比赛结束了才发现这个不符合二分性质)

注意到一个性质:对于 p=10,排列的所有可能仅有 10!=3628800 种,这意味着我们可以直接从排列中枚举,将复杂度降到 O(mp!p)。但问题是, n=2103 的规模,n2p! 是一个数量级的,也没啥用啊?

我们换一个角度考虑,在每次询问中,我们已经知道了原排列,需要将其变为一个按照升序来的新排列,那么,我们不难构造出这个操作序列是咋样的,之后直接在所有排列中查找这个操作序列是否存在即可。

Trie树

Trie树是为了实现字符串的快速检索,同样的,它也可以实现排列的检索。

不过这个Trie树应该开多大是应该先算好的(毕竟空间有限),我们可以,手建一棵排列长度为 4 的Trie树,计算一下大小看看。

我们记一个排列长度为 k 的Trie树的大小为 f(k)(按照节点数的数量来算),找规律发现:f(k)=kf(k1)+1,f(1)=2

这是一个阶乘增长的函数(查表发现 f(n)=k=0nn!k!),递推可得 f(10)=9864101,就离谱(我们得开一个将近 108 规模的 int 数组,还好内存限制是 512M,再加上很多叶子节点不需要向下扩展,所以实际上很多空间是未被使用的,所以不会被记入使用范围)

#include<bits/stdc++.h> using namespace std; const int N = 2010, SIZE = 1e7 + 10; struct State { int a[11]; State(){} State(int k, int *arr) { for (int i = 1; i <= k; ++i) a[i] = arr[i]; for (int i = k + 1; i <= 10; ++i) a[i] = i; } void Swap(int x, int y) { swap(a[x], a[y]); } }; // int Trie[SIZE][11], V[SIZE], tot = 0; void insert(State s, int val) { int p = 0, *arr = s.a; for (int i = 1; i <= 10; ++i) { int x = arr[i]; if (!Trie[p][x]) Trie[p][x] = ++tot, V[tot] = val; p = Trie[p][x]; V[p] = min(V[p], val); } } int search(int k, State s) { int p = 0, *arr = s.a; for (int i = 1; i <= k; ++i) { p = Trie[p][arr[i]]; if (!p) return -1; } return V[p]; } int n, m, x[N], y[N]; State opt[N][2]; int main() { //read scanf("%d%d", &n, &m); for (int i = 1; i <= n; ++i) scanf("%d%d", &x[i], &y[i]); //init for (int i = 1; i <= 10; ++i) opt[0][0].a[i] = i; insert(opt[0][0], 0); for (int i = 1; i <= n + 1; ++i) opt[i][0] = opt[0][0]; for (int len = 1; len <= n; ++len) { for (int i = 1; i + len - 1 <= n; ++i) { opt[i][len % 2] = opt[i + 1][(len - 1) % 2]; opt[i][len % 2].Swap(x[i], y[i]); insert(opt[i][len % 2], len); } } //query while (m--) { int k, a[11]; scanf("%d", &k); for (int i = 1; i <= k; ++i) scanf("%d", &a[i]); State s(k, a); printf("%d\n", search(k, s)); } return 0; }

康托展开+哈希

显然,我们可以将排列通过康托展开来映射为一个数,然后开一个哈希表来进行 O(1) 查找。

int calc(int n, int *arr) { int vis[20]; memset(vis, 0, sizeof(vis)); int res = 1; for (int i = 1; i < n; ++i) { int x = arr[i], cnt = 0; vis[x] = 1; for (int j = 1; j < x; ++j) if (!vis[j]) cnt++; //f[k]=k! res += cnt * f[n - i]; } return res; }

不过比较尴尬的是,我们构造出来的操作序列往往不止一种,很有可能是若干种(例如将排列 [6,4,2,5,3,1] 变为 [1,2,3,4,5,6],那么构造的操作序列应为 [6,4,2,5,3,1,,,,],即后四位不固定)。那么,我们总不能一个个枚举来查吧?

康托展开有一个比较让人平和的性质:假设一个排列的前 m 位确定了,后 nm 位不确定,那么记后 nm 位从小到大排序,所得康托展开值为 f+1,那么其从大到小排序所得的展开值为 f+(nm)!,且所有可能排列的区间值均在 [f+1,f+(nm)!] 之间。

我们可以开一棵线段树当哈希表,然后每次查询看看区间有没有符合要求的值即可。

//代码略,有空去参考下别的dalao写的代码

__EOF__

本文作者cyhforlight
本文链接https://www.cnblogs.com/cyhforlight/p/15968063.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   cyhforlight  阅读(134)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示