牛客寒假训练赛第一场
基本状况
赛时开了五题,B题大分讨卡住了,其他题目就看了题面。
有几个基本状况:
- 贪心题没有深入思考,就无脑二分入手,倒是大量罚时。
- 分讨思路不清楚。
- E题很搞,名字叫贪心题但是纯爆搜,爽切。
A
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";
}
E
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; }
B
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; }
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动态维护前缀和即可。
L
结论题,其实一开始想法是对的,贴地的照明范围最大,但是没啥信心也没做下去。
也是因为审题不认真,实际上墙和光源的距离是光源和背景墙的两倍,就是用中位线算就完全可以处理。
I
https://ac.nowcoder.com/acm/contest/67741/I
第一次见,毫无思路
核心思路
- 找到两种方法会使得哪个统计量有显著区别,尝试区分这个统计量的均值
- 如果这个统计量不好直接求,可以本地真的实现以下两种生成方式,然后生成大量数据,真的随机出这个统计量的真实值
- 比如求出法 \(1\) 该统计量均值 \(m_1\)、法 \(2\) 该统计量均值 \(m_2\),那对于输入数据,直接看输入 数据的该统计量 \(m\) 和 \(m_1\) 与 \(m_2\) 谁更近就行了。
太抽象了,是一种很新的题,似乎是模拟了机器学习的一些理论。
F
-
相关知识:第二类斯特林数
- 将 \(n\) 个 bit 每个 bit 看作一件物品
- 将 \(m\) 个数字每个看作一个集合
- 注意到这几乎就是第二类斯特林数 \(S(n,m)\) 的定义:\(n\) 个有区别球放到 \(m\) 个无区别盒子
- 还差在哪里呢?我们发现,本题的盒子(\(a_i\))并不是无区别的,但因为第二个条件要求了 每个盒子里的数从小到大有序,所以对于 \(S(n,m)\) 中的每一种方案,对其按 \(a_i\) 大小排序就得 到了本题的一种方案,且可以证明这是一种一一映射(重点是证明没有多对一)
- 因此,本题答案就是斯特林数 \(S(n,m)\)
- 第二类斯特林数的容斥求法,可以在log复杂度里做出这题
-
第二类斯特林数
-
将 \(n\) 个有区别的球放入 \(m\) 个无区别的盒子的方案数 \(S_{n,m}\)
- 有区别的定义
- \(C^3_5\) 五个有区别的球选三个
- \(1\) 五个没区别的数选三个
- 有区别的定义
-
\(S_{4,2}\)(此题要求盒子非空)
-
无区别的定义
- \((1,234)\) 和 \((234,1)\) 是同一种方案。
-
方案数为 \(7\)
- \((1,234)\)
- \((12,34)\)
- \((13,24)\)
- \((14,23)\)
- \((123,4)\)
- \((124,3)\)
- \((134,2)\)
-
若不要求非空,显然答案为 \(8\),即 \(m^n\).
-
若有 \(k\) 个为空,且盒子和球都有区别,则方案数为 \(C^k_m(m - k)^n\)
-
然后用容斥原理,容斥成非空个数(错排数),最后为了转化有区别为无区别,再除以 \(m!\)
\(S_(n,m) = \frac 1 {m!} \sum^m_k(-1)^kC^k_m(m - k)^n\)
-
-
H
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
通过这个参数与 \(w_i\) 进行按位与,显然能自动检测符合题解思路条件的物品,全部加上即可。
K
https://ac.nowcoder.com/acm/contest/67741/H
基环外向树?折磨!
大意:第 \(i\) 题的选项表明了 \(a_i\) 题的答案。
-
基环外向树
- \(n\) 条边
- 每个点出度为 \(1\)
- 即所有点必然入环
-
首先,每个 \(i\) 向 \(a_i\) 连一条有向边,表示 \(a_i\) 的答案被 \(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; }
- 首先所有连到环的链可以直接无视,它们不影响答案
D
https://ac.nowcoder.com/acm/contest/67741/D
通过题目分析出特性,进而剪枝
核心是要注意到:询问的 \(M\) 范围不大,所以数组稍微长一点儿,就很可能溢出 \(10^9\) 的范围,所以要考虑的方案其实很少;
- 最终数组,绝对值非 \(1\) 的最多 \(30\) 个
- 枚举哪个数变成 \(1\) 或 \(-1\),然后把此时数组乘积算出来,在枚举前先判断一下这 个数变的话能不能保证变后绝对值非 \(1\) 的最多 \(30\) 个,所以真正要枚举的数不多;
- \(𝑛≤30\),此时要么第一个数绝对值在 \(10^9\) 内、要么第二个数绝对值在 \(10^9\) 内,否则,前两个数乘积的绝对值一定大于 \(10^9\);因此我们 \(\sqrt {10^9}\times 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;
}
J
https://ac.nowcoder.com/acm/contest/67741/J
思维难度有点大
-
最关键的性质:考虑第 \(i\) 个任务结束时,一定有一个人在 𝑎𝑖 处;(听上去是废话)
- 这提示我们,只需要记录,另一个人可能的位置,可以用 set 来存;
-
具体做法
- 二分答案 \(mid\),
check
的写法是遍历所有的人物,用一个set存:此时,一个人在 𝑎𝑖 处,另一个人可能的位置集合; - 若某个时刻set为空,则check失败;否则,check成功;
- 注意向 set 里插入 \(a_i\) 的时机需要仔细考虑,一种正确的插法是
- 在 \(𝑎_{𝑖+1}\) 处判断若 $a_i - a_{i + 1} < 𝑚𝑖𝑑 $ 是否成立
- 若成立,插入 \(a_i\)
- 二分答案 \(mid\),
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;
}