牛客寒假训练赛第一场

1|0基本状况

赛时开了五题,B题大分讨卡住了,其他题目就看了题面。

有几个基本状况:

  • 贪心题没有深入思考,就无脑二分入手,倒是大量罚时。
  • 分讨思路不清楚。
  • E题很搞,名字叫贪心题但是纯爆搜,爽切。

2|0A

https://ac.nowcoder.com/acm/contest/67741/A

虽然签到题,但是学习一下 jly 写法。

  • 我的
void solve() { string s; cin >> s; int n; cin >> n; char cmp1[3] = {'d', 'f', 's'}; char cmp2[3] = {'D', 'F', 'S'}; short opt1 = 0, opt2 = 0; bool flag1 = false, flag2 = false; for (int i = 0; i < n; i++) { if (cmp1[opt1] == s[i]) opt1++; if (cmp2[opt2] == s[i]) opt2++; if (opt1 == 3) flag1 = true, opt1 = 0; if (opt2 == 3) flag2 = true, opt2 = 0; } cout << flag2 << ' ' << flag1 << endl; }
  • jly
void solve() { int n; std::cin >> n; std::string s; std::cin >> s; for (auto t : {"DFS", "dfs"}) { int k = 0; for (int i = 0; i < n; i++) { if (k < 3 && s[i] == t[k]) { k++; } } std::cout << (k == 3) << " "; } std::cout << "\n"; }

3|0E

https://ac.nowcoder.com/acm/contest/67741/B

继续向jly学习

  • 我的爆搜
void dfs(int step) { if (step == m) { vector<pair<int, int>> res(n + 1); for (int i = 1; i <= n; i++) { res[i].second = a[i], res[i].first = i; } sort(all1(res), [&](auto x1, auto x2) {if (x1.second == x2.second) return x1.first == 1;return x1.second > x2.second; }); for (int i = 1; i <= n; i++) if (res[i].first == 1) { ans = min(ans, i); break; } return; } a[u[step + 1]] += 3; dfs(step + 1); a[u[step + 1]] -= 3; a[v[step + 1]] += 3; dfs(step + 1); a[v[step + 1]] -= 3; a[u[step + 1]] += 1; a[v[step + 1]] += 1; dfs(step + 1); a[u[step + 1]] -= 1; a[v[step + 1]] -= 1; return; }; void solve() { cin >> n >> m; for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= m; i++) cin >> u[i] >> v[i]; ans = 200; a[u[1]] += 3; dfs(1); a[u[1]] -= 3; a[v[1]] += 3; dfs(1); a[v[1]] -= 3; a[u[1]] += 1; a[v[1]] += 1; dfs(1); a[u[1]] -= 1; a[v[1]] -= 1; cout << ans << endl; }
  • jly的爆搜
void solve() { int n, m; std::cin >> n >> m; std::vector<int> a(n); for (int i = 0; i < n; i++) { std::cin >> a[i]; } std::vector<int> u(m), v(m); for (int i = 0; i < m; i++) { std::cin >> u[i] >> v[i]; u[i]--, v[i]--; } int ans = n; auto dfs = [&](auto self, int i) -> void { if (i == m) { int res = 1; for (int j = 0; j < n; j++) { res += (a[j] > a[0]); } ans = std::min(ans, res); return; } for (auto [x, y] : {std::make_pair(3, 0), {0, 3}, {1, 1}}) { a[u[i]] += x;a[v[i]] += y; self(self, i + 1); a[u[i]] -= x;a[v[i]] -= y; } }; dfs(dfs, 0); std::cout << ans << "\n"; }

jly 这里有三点值得学习:

  • 可递归的函数写法,我一开始也是这样写但是CE了,要学一下。

    auto dfs = [&](auto self, int i) -> void {};//声明 self(self, i + 1);//函数内部调用 dfs(dfs, 0)//函数外部调用
  • 找该选手名次的方法,我用的是把选手和分数存入数组再排序,而jly更直接,直接比较大于该选手分数的人的数目。

    int res = 1; for (int j = 0; j < n; j++) { res += (a[j] > a[0]); }
  • 爆搜三种情况的写法,我是复制黏贴,复用性差,jly利用循环来简化代码,而且step传0参进去也不用在函数外写一遍针对step = 1的处理

    for (auto [x, y] : {std::make_pair(3, 0), {0, 3}, {1, 1}}) { a[u[i]] += x; a[v[i]] += y; self(self, i + 1); a[u[i]] -= x; a[v[i]] -= y; }

4|0B

https://ac.nowcoder.com/acm/contest/67741/B

神志不清,分讨树精

这题给我一个教训,这类大模拟不要无脑加特判,一定要归纳各种特殊性到一般性,尽量精简代码,特判加多了基本上就是偏了。

  • 我的

    void solve() { int n; ll r, c; map<pair<short, ll>, bool> tag; cin >> n; vector<pair<short, ll> > query(n); for(auto&[r, c] : query) { cin >> r >> c; r--; tag[make_pair(r, c)] = true; } if (n == 0) {cout << 3 << endl; return ;} sort(all(query), [&](auto x1, auto x2) {return x1.second < x2.second;}); int sum = 4, i = 0; int ldif = query.front().second < 0, rdif = query.back().second > 0; for (; i < n && query[i].second <= 0; i++) { auto [r, c] = query[i]; if (tag.count(make_pair(r ^ 1, c)) || tag.count(make_pair(r ^ 1, c - 1)) || tag.count(make_pair(r ^ 1, c + 1))) { ldif = 2; } } if (i > 0 && query[i - 1].second == 0) i--; for (; i < n && query[i].second >= 0; i++) { auto [r, c] = query[i]; if (tag.count(make_pair(r ^ 1, c)) || tag.count(make_pair(r ^ 1, c - 1)) || tag.count(make_pair(r ^ 1, c + 1))) { rdif = 2; } } sum -= ldif + rdif; if (tag.count(make_pair(1, 0))) { if (ldif == 2) { sum = min(sum, 1); } if (rdif == 2) sum = min(sum ,1); } sum = min<int>(sum, 3 - tag.count(make_pair(0, 1)) - tag.count(make_pair(0, -1)) - tag.count(make_pair(1, 0))); if (tag.count(make_pair(0, -1))) { if (rdif == 2) sum = min<int>(sum, 1); } if (tag.count(make_pair(0, 1))) { if (ldif == 2) sum = min<int>(sum, 1); } cout << sum << endl; }

    大方向肯定是没错的,用 pair<int,int> 来标记坐标,然后分讨。

    我的做法是先按正负坐标排序,然后左右分讨,最后在讨论三角形情况。


    过了两天,终于改出来了。

    其实当时思路的难点就是如果 (1,0) 下方有一个 (2,0),那么左边和右边就都只再需要一个,我当时的思路是最后再搞一大堆分讨:

  • 直接在 ldif,rdif 初始化的时候处理就行。

    int ldif = query.front().second < 0 || tag.count(make_pair(1, 0)), rdif = query.back().second > 0 || tag.count(make_pair(1, 0));

    因为上述情况其实就等价于第一种状况下左/右侧已经有一个着火点的情况了,所以这种情况下 ldif,rdif 初始值也是 1​。

  • 对循环范围的特判。

    for (; i < n && query[i].second <= 0; i++) { auto [r, c] = query[i]; if (tag.count(make_pair(r ^ 1, c)) || tag.count(make_pair(r ^ 1, c - 1)) || tag.count(make_pair(r ^ 1, c + 1))) { ldif = 2; } } if (i > 0 && query[i - 1].second == 0) i--; for (; i < n && query[i].second >= 0; i++) { auto [r, c] = query[i]; if (tag.count(make_pair(r ^ 1, c)) || tag.count(make_pair(r ^ 1, c - 1)) || tag.count(make_pair(r ^ 1, c + 1))) { rdif = 2; } }

    这是不行的,如果循环处理的时候存在 (2,0),那么如果是只有左/右边有一个着火点的状况,这样处理会让 ldif=rdif=2,显然错误。所以处理的时候不要管 0 的状况。

    for (; i < n && query[i].second < 0; i++) { auto [r, c] = query[i]; if (tag.count(make_pair(r ^ 1, c)) || tag.count(make_pair(r ^ 1, c - 1)) || tag.count(make_pair(r ^ 1, c + 1))) { ldif = 2; } } if (query[i].second == 0) i++; for (; i < n && query[i].second > 0; i++) { auto [r, c] = query[i]; if (tag.count(make_pair(r ^ 1, c)) || tag.count(make_pair(r ^ 1, c - 1)) || tag.count(make_pair(r ^ 1, c + 1))) { rdif = 2; } }
  • 最后再处理一下最大值为三这个性质

    sum = min<int>(sum, 3 - tag.count(make_pair(0, 1)) - tag.count(make_pair(0, -1)) - tag.count(make_pair(1, 0)));
  • 初版 AC

    void solve() { int n; ll r, c; map<pair<short, ll>, bool> tag; cin >> n; vector<pair<short, ll> > query(n); for(auto&[r, c] : query) { cin >> r >> c; r--; tag[make_pair(r, c)] = true; } if (n == 0) {cout << 3 << endl; return ;} sort(all(query), [&](auto x1, auto x2) {return x1.second < x2.second;}); int sum = 4, i = 0; int ldif = query.front().second < 0 || tag.count(make_pair(1, 0)), rdif = query.back().second > 0 || tag.count(make_pair(1, 0)); for (; i < n && query[i].second < 0; i++) { auto [r, c] = query[i]; if (tag.count(make_pair(r ^ 1, c)) || tag.count(make_pair(r ^ 1, c - 1)) || tag.count(make_pair(r ^ 1, c + 1))) { ldif = 2; } } if (query[i].second == 0) i++; for (; i < n && query[i].second > 0; i++) { auto [r, c] = query[i]; if (tag.count(make_pair(r ^ 1, c)) || tag.count(make_pair(r ^ 1, c - 1)) || tag.count(make_pair(r ^ 1, c + 1))) { rdif = 2; } } sum -= ldif + rdif; sum = min<int>(sum, 3 - tag.count(make_pair(0, 1)) - tag.count(make_pair(0, -1)) - tag.count(make_pair(1, 0))); cout << sum << endl; }

    进一步,发现 query 没有必要,直接遍历桶容器内元素即可,而且桶也没必要用 map,用 set 平替就行。

  • 优化 AC

    void solve() { int n; ll r, c; set<pair<short, ll>> tag; cin >> n; for(int i = 0; i < n; i++) { cin >> r >> c; r--; tag.insert(make_pair(r, c)); } int sum = 4; int ldif = tag.count(make_pair(1, 0)), rdif = tag.count(make_pair(1, 0)); for (auto[r, c] : tag) { if (c < 0) ldif = 1; if (c > 0) rdif = 1; } for (auto[r, c] : tag) { if (c == 0) continue; if (tag.count(make_pair(r ^ 1, c)) || tag.count(make_pair(r ^ 1, c - 1)) || tag.count(make_pair(r ^ 1, c + 1))) { if (c < 0) ldif = 2; if (c > 0) rdif = 2; } } sum -= ldif + rdif; sum = min<int>(sum, 3 - tag.count(make_pair(0, 1)) - tag.count(make_pair(0, -1)) - tag.count(make_pair(1, 0))); cout << sum << endl; }

5|0G

https://ac.nowcoder.com/acm/contest/67741/G

一开始写成对每一段进行二分,正确性应该没问题但是太复杂了,不知道哪里细节错误。

后面发现这题可以直接贪心求答案

已经犯过很多次这类错误,当一个做法很绕很复杂的时候,要考虑能不能换做法

贪心思路很简单,维护前缀和判断就行。

但是这题之所以要写个博客,是因为这种只要维护一个前缀和的题目,我一直在无脑写前缀和数组。

老样子。

  • 我的
void solve() { int n, m; cin >> n >> m; vector<pair<ll, ll> > a(n); for (int i = 0; i < n; i++) cin >> a[i].first >> a[i].second; vector<ll> s(n); sort(all(a)); for (int i = 0; i < n; i++) if (i == 0) s[i] = a[i].second; else s[i] = s[i - 1] + a[i].second; ll ans = m; for (int i = 0; i < n; i++) { if (m + s[i] >= a[i].first) ans = max(ans, m + s[i]); } cout << ans << endl; }
  • jly
void solve() { int n, m; std::cin >> n >> m; std::vector<std::pair<int, int>> a(n); for (int i = 0; i < n; i++) { int x, y; std::cin >> x >> y; a[i] = {x, y}; } std::sort(a.begin(), a.end()); i64 ans = m; i64 sum = 0; for (auto [x, y] : a) { sum += y; if (x - sum <= m) { ans = std::max(ans, sum + m); } } std::cout << ans << "\n"; }

用一个sum动态维护前缀和即可。

6|0L

https://ac.nowcoder.com/acm/contest/67741/L

结论题,其实一开始想法是对的,贴地的照明范围最大,但是没啥信心也没做下去。

也是因为审题不认真,实际上墙和光源的距离是光源和背景墙的两倍,就是用中位线算就完全可以处理。

7|0I

https://ac.nowcoder.com/acm/contest/67741/I

第一次见,毫无思路

核心思路

  1. 找到两种方法会使得哪个统计量有显著区别,尝试区分这个统计量的均值
  2. 如果这个统计量不好直接求,可以本地真的实现以下两种生成方式,然后生成大量数据,真的随机出这个统计量的真实值
  3. 比如求出法 1 该统计量均值 m1、法 2 该统计量均值 m2,那对于输入数据,直接看输入 数据的该统计量 mm1m2 谁更近就行了。

太抽象了,是一种很新的题,似乎是模拟了机器学习的一些理论。

8|0F

https://ac.nowcoder.com/acm/contest/67741/F

  • 相关知识:第二类斯特林数

    • n 个 bit 每个 bit 看作一件物品
    • m 个数字每个看作一个集合
    • 注意到这几乎就是第二类斯特林数 S(n,m) 的定义:n 个有区别球放到 m 个无区别盒子
    • 还差在哪里呢?我们发现,本题的盒子(ai)并不是无区别的,但因为第二个条件要求了 每个盒子里的数从小到大有序,所以对于 S(n,m) 中的每一种方案,对其按 ai 大小排序就得 到了本题的一种方案,且可以证明这是一种一一映射(重点是证明没有多对一)
    • 因此,本题答案就是斯特林数 S(n,m)
    • 第二类斯特林数的容斥求法,可以在log复杂度里做出这题
  • 第二类斯特林数

    • n 个有区别的球放入 m 个无区别的盒子的方案数 Sn,m

      • 有区别的定义
        • C53​ 五个有区别的球选三个
        • 1 五个没区别的数选三个
    • S4,2(此题要求盒子非空)

      • 无区别的定义

        • (1,234)(234,1) 是同一种方案。
      • 方案数为 7

        • (1,234)
        • (12,34)
        • (13,24)
        • (14,23)
        • (123,4)
        • (124,3)
        • (134,2)
      • 若不要求非空,显然答案为 8,即 mn.

      • 若有 k 个为空,且盒子和球都有区别,则方案数为 Cmk(mk)n

      • 然后用容斥原理,容斥成非空个数(错排数),最后为了转化有区别为无区别,再除以 m!

        S(n,m)=1m!km(1)kCmk(mk)n

9|0H

https://ac.nowcoder.com/acm/contest/67741/H

位运算好题

  • 先考虑每个物品的重量都只含 1bit​ 的情况,可以帮助对这题要解决什么问题有个大致了解
  • 记所选物品重量或起来是 𝑐枚举 𝑚 里是 1 的某个 bit,强制 𝑐 里该位为 0,则该位将 𝑚 分成了前后两部分
    • 对于前面的那部分位(更高位),要求所选的物品这些位必须是𝑚的子集(即𝑚对应位是 1 才能选)
    • 对于后面的那部分位(更低位),没有任何限制
  • 因此,枚举 𝑚 里每一位作为这个分界,每个物品就变成了要么能选要么不能选、彼此之间也不影响,所以把能选的都选上就好,最后再特判一下 c=m 的状况,即可保证枚举了所有情况
void solve() { int n, m; cin >> n >> m; vector<int> v(n), w(n); for (int i = 0; i < n; i++) cin >> v[i] >> w[i]; ll ans = 0; auto get = [&](int s) { ll res = 0; for (int i = 0; i < n; i++) { if ((s & w[i]) == w[i]) { res += v[i]; } } ans = max(ans, res); }; for (int i = 29; i >= 0; i--) { if (m >> i & 1) { get((m ^ (1 << i)) | ((1 << i) - 1));//很关键 } } get(m);//处理C=M的情况 cout << ans << endl; }

get函数内穿的参数用了几个 trick.

  • m ^ (1 << i)//把 m 的第 i 位取反,这里是置为 0
  • (m ^ (1 << i) | ((1 << i) - 1))//把 i 位之前(右边)的位数全部变成 1,因为由题解,更低位不限制 //1 << i 是第 i 位为 1,右边都是 0 //1 << i - 1 则会把第 i 位变成 0, 右边全部变成 1

通过这个参数与 wi 进行按位与,显然能自动检测符合题解思路条件的物品,全部加上即可。

10|0K

https://ac.nowcoder.com/acm/contest/67741/H

基环外向树?折磨!

大意:第 i 题的选项表明了 ai 题的答案。

  • 基环外向树

    • n 条边
    • 每个点出度为 1
    • 即所有点必然入环
  • 首先,每个 iai 连一条有向边,表示 ai 的答案被 i 限制住了;该题的依赖性质构成了基环外向树森林

    • 首先所有连到环的链可以直接无视,它们不影响答案
      • 因为这个链所连接的环那个点确定答案后、链上每个点的答案可以由此反向传播依次得到(至于为何每个点答案唯一?这是因为该点走到环的路径上所有排列复合得到的仍是排列),因此,环上点确定方案、则链也随之确定唯一一种方案,所以可以忽略链
    • 对于环,我们随机选个起点,暴力枚举这个点选 ABCDE 中哪个选项然后暴力模拟来 check 这个选项是否能让这个环满足条件即可
    • 因此,每个基环内向树的答案是0~5之间整数
    • 最后的答案是每个基环内向树答案的乘积
    int main() { int n; std::cin >> n; std::vector<int> a(n); std::vector<std::string> s(n); for (int i = 0; i < n; i++) { std::cin >> a[i] >> s[i]; a[i]--; } Z ans = 1; std::vector<int> vis(n, -1); for (int i = 0; i < n; i++) { int j = i; while (vis[j] == -1) { vis[j] = i; j = a[j]; } if (vis[j] == i) { int k = j; int len = 0; do { k = a[k]; len++; } while (k != j); int res = 0; for (int x = 0; x < 5; x++) { int v = x; for (int i = 0; i < len; i++) { v = s[k][v] - 'A'; k = a[k]; } res += v == x; } ans *= res; } } std::cout << ans << "\n"; return 0; }

11|0D

https://ac.nowcoder.com/acm/contest/67741/D

通过题目分析出特性,进而剪枝

核心是要注意到:询问的 M 范围不大,所以数组稍微长一点儿,就很可能溢出 109 的范围,所以要考虑的方案其实很少;

  • 最终数组,绝对值非 1 的最多 30
    • 枚举哪个数变成 11,然后把此时数组乘积算出来,在枚举前先判断一下这 个数变的话能不能保证变后绝对值非 1 的最多 30​ 个,所以真正要枚举的数不多;
    • 𝑛30,此时要么第一个数绝对值在 109 内、要么第二个数绝对值在 109 内,否则,前两个数乘积的绝对值一定大于 109;因此我们 109×n 的暴力枚举就可以;
#include <bits/stdc++.h> using i64 = long long; constexpr int K = 4E4; constexpr int inf = 1E9; int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); int n, Q; std::cin >> n >> Q; std::map<int, int> cnt; for (int i = 0; i < n; i++) { int a; std::cin >> a; cnt[a] += 1; } std::set<int> S{0}; if (cnt.size() <= 20) { std::vector<std::pair<int, int>> a(cnt.begin(), cnt.end()); n = a.size(); int x = a[0].first - K; for (int i = 0; i < n; i++) { for (x = std::max(x, a[i].first - K); x <= a[i].first + K; x++) { i64 res = 1; for (int j = 0; j < n; j++) { int v = a[j].first - x; if (v == 1) { continue; } if (v == -1) { if (a[j].second % 2) { res *= -1; } continue; } for (int c = 0; c < a[j].second; c++) { res *= v; if (std::abs(res) > inf) { break; } } if (std::abs(res) > inf) { break; } } if (std::abs(res) <= inf) { S.insert(res); } } } } while (Q--) { int M; std::cin >> M; if (S.count(M)) { std::cout << "Yes\n"; } else { std::cout << "No\n"; } } return 0; }

12|0J

https://ac.nowcoder.com/acm/contest/67741/J

思维难度有点大

  • 最关键的性质:考虑第 i​ 个任务结束时,一定有一个人在 𝑎𝑖 处;(听上去是废话)

    • 这提示我们,只需要记录,另一个人可能的位置,可以用 set 来存;
  • 具体做法

    • 二分答案 mid​,check 的写法是遍历所有的人物,用一个set存:此时,一个人在 𝑎𝑖 处,另一个人可能的位置集合;
    • 若某个时刻set为空,则check失败;否则,check成功;
    • 注意向 set 里插入 ai 的时机需要仔细考虑,一种正确的插法是
      • 𝑎𝑖+1 处判断若 aiai+1<𝑚𝑖𝑑​ 是否成立
      • 若成立,插入 ai
void solve() { int n, x, y; cin >> n >> x >> y; vector<int> a(n); for (auto&i : a) cin >> i; auto check = [&](int d) { int last = y; set<int> S; if (abs(x - y) <= d) S.insert(x); for (auto x : a) { if (!S.empty() && abs(x - last) <= d) S.insert(last); while(!S.empty() && *S.begin() < x - d) S.erase(S.begin()); while(!S.empty() && *S.rbegin() > x + d) S.erase(*S.rbegin());//rebegin()倒叙迭代的begin,同理还有rend(),并且rbegin() < rend() last = x; } return !S.empty(); }; int lo = 0, hi = 1E9; while(lo <= hi) { int mid = lo + hi >> 1; if (check(mid)) hi = mid - 1; else lo = mid + 1; } cout << lo << endl; }

__EOF__

本文作者Kdlyh
本文链接https://www.cnblogs.com/kdlyh/p/18005064.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   加固文明幻景  阅读(11)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示