2025牛客寒假算法基础集训营3
A
手玩秒了:
奇数 yes 偶数 no
B
题目大意
- 只剩这一个小球
- 这个小球的数和顺时针方向上的下一个小球的数之和是个质数
问是否存在一种拿球的顺序使得最终能够拿掉所有的球,有还需要输出顺序。
解题思路
若
在环上我们就在环上的
以上做法涉及到环形 dp 的一个定理:
在无后效性的前提下,任何一个环上的 dp 转移关系,一定等价于考虑所有以
为起点的 条链上 dp 转移关系的并集。
CODE
点击查看代码
bool vis[M + 5];
void solve()
{
int n = 0;
std::cin >> n;
std::vector<int> a(n, 0);
for (auto &i : a) {
std::cin >> i;
}
int idx = -1;
std::deque<int> q;
for (int i = 0, j = 0; i < n && idx == -1; i++) {
while (j - i < n) {
while (not q.empty() && not vis[a[q.back() % n] + a[j % n]]) {
q.pop_back();
}
q.push_back(j);
j++;
}
while (not q.empty() && q.front() < i) {
q.pop_front();
}
if (q.size() == 1) {
idx = i;
}
}
if (~idx) {
std::cout << "Yes\n";
int r = idx;
std::stack<int> stk;
while (r - idx < n) {
while (not stk.empty() && not vis[a[stk.top() % n] + a[r % n]]) {
std::cout << stk.top() % n << ' ';
stk.pop();
}
stk.push(r);
r++;
}
std::cout << stk.top() % n << '\n';
}
else {
std::cout << "No\n";
}
return;
}
int main()
{
std::vector<int> pri;
for (int i = 2; i <= M; i++) {
if (not vis[i]) {
pri.push_back(i);
}
for (auto &p : pri) {
if (p * i > M) {
break;
}
vis[p * i] = true;
if (i % p == 0) {
break;
}
}
}
IOS;
int _t = 1;
// std::cin >> _t;
while (_t--)
{
solve();
}
sp();
return 0;
}
C
题目大意
有一单词表,上面有 abcd
,这不代表你敲出了 bcd
,当且仅当记事本上是 bcd
才算。问至少需要敲击多少下才能敲出所有单词。
解题思路
在建立了字典树后,题目的描述就可以这样理解:
- 敲击字母就往叶子的方向走
- 退格就是往回走
于是最少的敲击数就是:字典树的节点数
CODE
点击查看代码
constexpr int N = 1e5, M = 1e6, Inf = 1e9;
int tr[M + 1][26], tot;
void insert(std::string s) {
int cur = 0;
for (auto &c : s) {
if (tr[cur][c - 'a'] == 0) {
tr[cur][c - 'a'] = ++tot;
}
cur = tr[cur][c - 'a'];
}
}
void solve()
{
int n = 0, m = 0;
std::cin >> n >> m;
int mx = 0;
for (int i = 0; i < n; i++) {
std::string s;
std::cin >> s;
mx = std::max(mx, int(s.length()));
insert(s);
}
int l = 0, r = 0;
std::cin >> l >> r;
std::cout << tot * 2 - mx << '\n';
}
D
题目大意
题目背景同上一题,不同的地方在于,上一题要求敲出全部的
解题思路
上一题的答案是:字典树的节点数
如果我们在建树插入单词的过程中给树上的节点上色(对已经上过色的节点进行覆盖),那么统计字典树的节点数就变成了统计不同颜色的节点数之和。假设我们已经插入到了第
统计颜色只需要一种支持单点修改和区间查询的数据结构就好了,比如树状数组。
CODE
点击查看代码
constexpr int N = 1e5, M = 1e5, L = 1e6, T = 100;
int tr[L + 5][27];
int tot = 0;
class BIT {
private:
int n;
std::vector<int> a;
int lowbit(int x) {
return x & -x;
}
int sum(int pos) {
int res = 0;
for (int i = pos; i > 0; i -= lowbit(i)) {
res += a[i];
}
return res;
}
public:
BIT(int _n) {
n = _n;
a.assign(n + 1, 0);
}
void add(int pos, int val) {
for (int i = pos; i <= n; i += lowbit(i)) {
a[i] += val;
}
}
int sum(int l, int r) {
return sum(r) - sum(l - 1);
}
};
void insert(std::string s, int id, BIT &cnt) {
int cur = 0;
for (auto &c : s) {
int to = c - 'a';
if (tr[cur][to] == 0) {
tr[cur][to] = ++tot;
}
else {
cnt.add(tr[tr[cur][to]][26], -1);
}
cur = tr[cur][to];
tr[cur][26] = id;
}
cnt.add(id, s.length());
}
int main () {
int n = 0, m = 0;
std::cin >> n >> m;
std::vector s(n + 1, std::string{});
std::vector ans(m, 0);
BIT cnt(n);
for (int i = 1; i <= n; i++) {
std::cin >> s[i];
}
std::vector q(n + 1, std::vector<std::array<int, 2>>{});
for (int i = 0; i < m; i++) {
int l = 0, r = 0;
std::cin >> l >> r;
q[r].push_back({ l, i });
}
std::pair<int, int> stk[n];
int top = 0;
for (int r = 1; r <= n; r++) {
insert(s[r], r, cnt);
int len = s[r].length();
while (top && stk[top - 1].second <= len) {
top--;
}
stk[top++] = { r, len };
for (auto &[l, id] : q[r]) {
ans[id] = (cnt.sum(l, r)) * 2 - (*std::lower_bound(stk, stk + top, std::make_pair(l, 0))).second;
}
}
for (auto &i : ans) {
std::cout << i << '\n';
}
return 0;
}
E
题目大意
在数轴上有若干不计体积质量相同的小球,每个小球的初始位置不同且速率都为每秒一个单位,速度的方向为数轴的正方向或者反方向。且小球之间的碰撞都为完全弹性碰撞(即在质量相同的情况下碰撞后发生速度交换)。问发生
解题思路
首先利用相对的思想,我们可以认为所有速度方向为正的都静止,而反方向的就以每秒两个单位运动。这样我们就可以很容易的二分时间长度来找到答案了。为了在二分的时候不涉及小数,对于当前正在二分的 chk
函数就是对于每个速度方向为正的小球,算它前面
注意最后二分出来的答案还要除二。
CODE
点击查看代码
void solve()
{
int n = 0, k = 0;
std::cin >> n >> k;
std::vector<int> a, b;
for (int i = 0; i < n; i++) {
int p = 0, v = 0;
std::cin >> p >> v;
if (~v) {
a.push_back(p);
}
else {
b.push_back(p);
}
}
std::sort(a.begin(), a.end());
std::sort(b.begin(), b.end());
// 0.5s 的数量
int l = 0, r = Inf;
while (l <= r) {
int m = l + r >> 1;
int num = 0;
for (auto &p : a) {
num += std::upper_bound(b.begin(), b.end(), p + m) - std::upper_bound(b.begin(), b.end(), p);
}
if (num >= k) {
r = m - 1;
}
else {
l = m + 1;
}
}
if (r == Inf) {
std::cout << "No\n";
}
else {
std::cout << "Yes\n";
double ans = l;
ans /= 2.0;
std::cout << std::fixed << std::setprecision(6) << ans << '\n';
}
}
F
略
G
题目大意
有一个长度为
解题思路
当然不可能直接把整个序列求出来再排序。
通过打表找规律可以发现未排序的整个序列
令
于是我们就去二分
CODE
点击查看代码
// 0 首项
// 1 公差
// 2 项数
using seq = std::array<int, 3>;
void solve()
{
int n = 0, k = 0;
std::cin >> n >> k;
std::vector<seq> s;
int mx = 0;
for (int i = 1; i <= n; i++) {
int f = n / i, j = n / f;
mx = std::max(mx, n % i);
s.push_back({ n % i, -f, j - i + 1 });
i = j;
}
// for (auto &[x, y, z] : s) {
// std::cout << x << ' ' << y << ' ' << z << '\n';
// }
// 二分求一个数 x 使得大于等于 x 的数都可以选而 x - 1 只能尽可能多的选
int l = 0, r = mx;
while (l <= r) {
int mid = l + r >> 1;
int cnt = 0;
for (auto &[a, d, m] : s) {
if (mid <= a) {
int num = std::min(m, (mid - a) / d + 1);
cnt += num;
}
}
if (cnt > k) {
l = mid + 1;
}
else {
r = mid - 1;
}
}
i64 ans = 0;
for (auto &[a, d, m] : s) {
if (l <= a) {
int num = std::min(m, (l - a) / d + 1);
k -= num;
int b = a + d * (num - 1);
ans += 1ll * (a + b) * num / 2;
}
}
ans += 1ll * k * r;
std::cout << ans << '\n';
return;
}
H
题目大意
有一颗
解题思路
要是只问给出的原始树的权值,我们就可以直接以任意一个点为根节点树形 dp。具体的我们需要在 dp 的过程中计算以下值:
, 表示以 为根的子树的权值 , 表示以 为根的子树中,有多少路径是以 为一个端点,另一个端点是白色(黑色)。 表示以 为根的子树中, 满足一个端点是 而另一个端点是白色(黑色)的路径的总节点数(可以看成一个节点就对应一条边,这样好转移一些)。
于是对于节点
特别要注意转移中的
这样我们就计算出了一半的答案:对于边
在完成一次 dp 得出一半的答案后,就来到了本题最精妙的操作:父亲节点和儿子节点的倒置。观察到转移的几个式子是可逆的,即从 5 式开始将所有的 + 号变成 - 号(注意 2 式中的 - 号不用变),就可以撤销从子节点
CODE
点击查看代码
std::string col;
std::vector<int> g[N];
std::map<std::pair<int, int>, int> idx;
std::array<i64, 2> ans[N];
// 以 i 为一个端点另一端是黑(白)路径的数量
// 以 i 为一个端点另一端是黑(白)路径的总长度
std::array<i64, 2> cnt[N], tot[N];
i64 dp[N]; // 结果
// u 是 v 的父节点, 将 v 转移到 u。
void trans(int u, int v) {
dp[u] += dp[v] + cnt[u][0] * tot[v][1] + cnt[u][1] * tot[v][0];
dp[u] += cnt[v][0] * (tot[u][1] - cnt[u][1]) + cnt[v][1] * (tot[u][0] - cnt[u][0]);
tot[u][0] += tot[v][0] + cnt[v][0];
tot[u][1] += tot[v][1] + cnt[v][1];
cnt[u][0] += cnt[v][0];
cnt[u][1] += cnt[v][1];
}
// u 是 v 的父节点, 将 v 变成父亲
void inv(int u, int v, bool W) {
tot[u][0] -= tot[v][0] + cnt[v][0];
tot[u][1] -= tot[v][1] + cnt[v][1];
cnt[u][0] -= cnt[v][0];
cnt[u][1] -= cnt[v][1];
dp[u] -= dp[v] + cnt[u][0] * tot[v][1] + cnt[u][1] * tot[v][0];
dp[u] -= cnt[v][0] * (tot[u][1] - cnt[u][1]) + cnt[v][1] * (tot[u][0] - cnt[u][0]);
// 写入答案
if (W) {
auto pos = idx.find({ u, v });
if (pos != idx.end()) {
ans[(*pos).second] = { dp[u], dp[v] };
}
else {
ans[(*idx.find({ v, u })).second] = { dp[v], dp[u] };
}
}
trans(v, u);
}
void dfs1(int cur, int fa) {
cnt[cur][(col[cur] == 'b' ? 0 : 1)] = 1;
tot[cur][(col[cur] == 'b' ? 0 : 1)] = 1;
for (auto &to : g[cur]) {
if (to != fa) {
dfs1(to, cur);
trans(cur, to);
}
}
}
void dfs2(int cur, int fa) {
for (auto &to : g[cur]) {
if (to != fa) {
inv(cur, to, true);
dfs2(to, cur);
inv(to, cur, false);
}
}
}
void solve()
{
int n = 0;
std::cin >> n >> col;
for (int i = 0; i < n - 1; i++) {
int u = 0, v = 0;
std::cin >> u >> v;
u--, v--;
g[u].push_back(v);
g[v].push_back(u);
idx[{ u, v }] = i;
}
dfs1(0, 0);
dfs2(0, 0);
for (int i = 0; i < n - 1; i++) {
std::cout << ans[i][0] << ' ' << ans[i][1] << '\n';
}
return;
}
I
题目大意
在数轴上有若干不重复的特殊点,可以选择一个起点和长度
解题思路
我们直接任意选择两个点,并且假设最优解经过这两个点。于是我们就可以枚举这两点间距离的质因数来作为步长,计算在经过选定的两个点的前提下,最多还可以经过多少个点。这样选点计算 100 百次,可以认为一定能得到最优解。
以下是证明:
首先很容易想到过特殊点数量的一个下界:
CODE
点击查看代码
std::mt19937_64 rnd(std::chrono::steady_clock::now().time_since_epoch().count());
int main() {
for (int i = 2; i * i < N; i++) {
if (not vis[i]) {
pri.push_back(i);
}
for (auto &p : pri) {
int d = p * i;
if (d * d >= N) {
break;
}
vis[d] = true;
if (i % p == 0) {
break;
}
}
}
int n = 0;
std::cin >> n;
std::vector a(n, 0);
for (auto &i : a) {
std::cin >> i;
}
if (n == 1) {
std::cout << a[0] << ' ' << 2 << '\n';
return 0;
}
std::array<int, 3> ans = { a[0], 2, 0 };
for (int t = 0; t < T; t++) {
int x = a[rnd() % n], y = 0;
do {
y = a[rnd() % n];
} while (x == y);
int d = std::abs(x - y);
for (auto &p : pri) {
if (p * p > d) {
break;
}
if (d % p == 0) {
while (d % p == 0) {
d /= p;
}
int cnt = 0;
for (auto &i : a) {
if ((x - i) % p == 0) {
cnt++;
}
}
if (cnt > ans[2]) {
ans = { x % p, p, cnt };
}
}
}
if (d != 1) {
int cnt = 0;
for (auto &i : a) {
if ((x - i) % d == 0) {
cnt++;
}
}
if (cnt > ans[2]) {
ans = { x % d, d, cnt };
}
}
}
std::cout << ans[0] << ' ' << ans[1] << '\n';
return 0;
}
J
模拟,建议直接看官方题解。
K
题目大意
给定
合并的要求是:序列内的顺序不能变,但中间可以插入别的序列里的数。
保证:
- 这
个序列的值域互不相交 - 序列的长度和
解题思路
首先序列内的顺序不变,所以我们就可以独立先计算出每个序列的逆序数,他们的和就是有解的
于是对于界内的
CODE
点击查看代码
void solve()
{
int n = 0, k = 0;
std::cin >> n >> k;
i64 mn = 0; // 下界
std::vector v(n, std::vector<int>{});
for (auto &a : v) {
int l = 0;
std::cin >> l;
a.assign(l, 0);
for (auto &i : a) {
std::cin >> i;
}
// 计算 a 中的逆序对的数量
for (int i = 0; i < l; i++) {
for (int j = i + 1; j < l; j++) {
if (a[i] > a[j]) {
mn++;
}
}
}
}
// 值域大的序列在前面
std::sort(v.begin(), v.end(), [&](const auto &x, const auto &y) {
return x[0] > y[0];
});
i64 mx = mn; // 上界
int len = 0;
for (auto &a : v) {
mx += len * a.size();
len += a.size();
}
if (k < mn || k > mx) {
std::cout << "No\n";
return;
}
else {
std::cout << "Yes\n";
}
// 只关注需要通过合并序列添加多少逆序对
k -= mn;
std::array<int, 2> idx = { -1, -1 };
for (int i = 0; i < n && (idx[0] == -1); i++) { // 从值域最大序列的开始,尝试将其中的数放在前面
len -= v[i].size(); // 放一个数导致的逆序数增量
for (int j = 0; j < v[i].size(); j++) {
if (len > k) { // 如果放在前面的增量多了,就记录下现在的位置,准备在后面找一个位置给它。
idx = { i, j };
break;
}
else { // 否则就直接放在前面
k -= len;
std::cout << v[i][j] << ' ';
}
}
}
// 为了不额外增加新的逆序对,还未开始合并的序列就直接正序输出
for (int i = n - 1; (~idx[0]) && i > idx[0]; i--) {
for (int j = 0; j < v[i].size(); j++) {
std::cout << v[i][j] << ' ';
len--; // 在此处插入前面记录的数对逆序数的增量
if (len == k) { // 正好合适就插在此处,此逆序对剩下的数就放在最后面
std::cout << v[idx[0]][idx[1]] << ' ';
}
}
}
// 最后的数
for (int i = idx[1] + 1; (~idx[0]) && i < v[idx[0]].size(); i++) {
std::cout << v[idx[0]][i] << ' ';
}
std::cout << '\n';
return;
}
L
找规律有点恶心,感觉还不如去打表。
以下是非打表代码。
CODE
点击查看代码
void solve()
{
int n = 0;
std::cin >> n;
std::vector idx(n + 1, std::vector<int>{});
for (int i = 0, m = 1; i <= n; i++) {
for (int j = 0; j <= i; j++) {
// std::cout << m << '\t';
idx[i].push_back(m++);
}
// std::cout << '\n';
}
std::cout << "Yes\n";
for (int i = 0; i + 1 < n; i++) {
std::cout << idx[i][0] << ' ';
}
for (int j = 0; j < n; j++) {
std::cout << idx[n - 1][j] << ' ' << idx[n][j] << ' ' << idx[n][j + 1] << ' ' << idx[n - 1][j] << ' ';
}
for (int i = n - 2; i >= 0; i--) {
std::cout << idx[i].back() << ' ';
for (int j = i - 1; j >= 0; j--) {
std::cout << idx[i + 1][j + 1] << ' ' << idx[i][j] << ' ';
}
for (int j = 1; j <= i; j++) {
std::cout << idx[i][j] << ' ';
}
}
std::cout << '\n';
return;
}
M
略
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话