250705 学习笔记
前言
知末便知径,径尽方晓终。
... if R is a node on the minimal path from P to Q, knowledge of the latter implies the knowledge of the minimal path from P to R. —— Edsger W. Dijkstra
模拟赛
八点整开始打模拟赛,十一点结束。打了 T1 的 100 分暴力,T2 的 100 分暴力,T3 的 100 分暴力,没有注意到 T6 的 100 分暴力,最后以 361 分收尾。总结一下,没有挂分,算是……
| # | 总 | A | B | C | D | E | F | G |
|---|---|---|---|---|---|---|---|---|
| 6 | 361 | 100 | 100 | 100 | 11 | 50 | - | - |
| - | 03:00:00 | 00:07:36 | 00:48:49 | 01:50:28 | 16:16:16 | 06:30:19 | 08:24:49 | 99:99:99 |
A. 寻宝游戏
分别计算到两边的步数,取较小者即可。
B. 路灯
分类讨论:1. 两侧的路灯;2. 两路灯中间。还是比较好写的。
C. 收集彩球
首先注意到,可以将颜色看成结点,盒子中上方球的颜色和下方球的颜色之间连边,这样就可以将盒子中的球上下关系转化为一张图。图中有若干连通块,每个块内的颜色球移动不影响块外的球答案(因为块内的颜色和块外不需要也不能进行移动)。所以考虑分别处理每个连通块。下文中,符合要求是指每种颜色的两个球都在同一个盒子中。
可以发现,想要让块内颜色球位置符合要求,需要先将某两种颜色球移动到空盒(\(2\) 步),再将其它颜色球依次配对(每种颜色需要 \(1\) 步),最后空出一个盒子。因此,若一个连通块内有 \(x\) 种颜色且可以符合要求,让它们符合要求需要:
- \(0\) 步,如果 \(x=1\)(本来就是符合要求的);
- \(x + 1\) 步,如果 \(x>1\)。
现在考虑验证某个块通过移动符合要求可行性的方法。注意到只有 \(1\) 个空盒,所以同一时间最多转移出 \(2\) 个球。因此,考虑设第 \(i\) 种颜色球在上方的盒子数 \(u_i\)。根据题意,\(u_i\) 的取值为 \(0,1\) 和 \(2\)。如果一个连通块内存在 \(2\) 种颜色的 \(u_i=2\),则只用一个空盒无法完成移动。可以基于这个特性对连通块进行验证。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int n, a[N][5];
int fa[N];
int getfa(int x) {return (x == fa[x]) ? x : (fa[x] = getfa(fa[x]));}
void merge(int x, int y) {
x = getfa(x), y = getfa(y);
if (x != y) fa[x] = y;
}
int cnt[N];
vector<int> g[N];
int up[N], pos[N][5];
bool nok(int x) { // 判断连通块是否不符合要求
int flag = 0;
for (int i = 0; i < g[x].size(); i++) {
if (up[g[x][i]] == 2) {
if (flag) return true;
flag = g[x][i];
}
}
return false;
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) fa[i] = i;
for (int i = 1; i <= n; i++) {
cin >> a[i][1] >> a[i][2];
merge(a[i][1], a[i][2]);
up[a[i][1]]++;
pos[a[i][1]][1 + (pos[a[i][1]][1] != 0)] = i;
}
for (int i = 1; i <= n; i++) {
int tmp = getfa(i);
cnt[tmp]++;
g[tmp].push_back(i);
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (cnt[i] == 0 || cnt[i] == 1) continue;
if (nok(i)) {
cout << -1 << '\n';
return 0;
}
ans += cnt[i] + 1;
}
cout << ans << '\n';
return 0;
}
D. 双 v 字形涂色
分类讨论。
- 考虑棋盘染色(这里用红、绿),若选择红、绿格各一个,则互不产生影响。
- 考虑一个格在另一个格的直角内。此时两个格互不影响。
- 若两个 v 交叉,则分别考虑左撞右和右撞左。注意到,第一种情况中的两个部分可以被一条 \(x-y=k\) 的直线分隔开,第二种情况中的两个部分可以被一条 \(x+y=k\) 的直线分隔开。
将三种情况的答案取最大值得到答案。可以使用动态规划。注意细节处理。
#include <bits/stdc++.h>
using namespace std;
const int N = 3005;
int n, m, a[N][N], ul[N][N], ur[N][N], v[N][N], b[N][N], lv[N][N], rv[N][N], plv[2 * N], prv[2 * N], nlv[2 * N], nrv[2 * N];
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
string s;
cin >> s;
for (int j = 1; j <= m; j++) a[i][j] = s[j - 1] - '0';
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i][j]) {
ul[i][j] = ul[i - 1][j - 1] + 1;
ur[i][j] = ur[i - 1][j + 1] + 1;
v[i][j] = ul[i][j] + ur[i][j] - 1;
}
b[i][j] = max({b[i - 1][j - 1], b[i - 1][j], b[i - 1][j + 1], v[i][j]});
}
}
int x = 0, y = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if ((i + j) & 1) x = max(x, v[i][j]);
else y = max(y, v[i][j]);
}
}
int ans = x + y; // odd+eve
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
ans = max(ans, v[i][j] + b[i - 1][j]);
}
}
for (int i = n; i >= 1; i--) {
for (int j = 1; j <= m; j++) {
if (a[i][j]) {
lv[i][j] = max(ul[i][j], lv[i + 1][j - 1] + 1);
rv[i][j] = max(ur[i][j], rv[i + 1][j + 1] + 1);
plv[j + n - i + 1] = max(plv[j + n - i + 1], lv[i][j]);
prv[i + j] = max(prv[i + j], rv[i][j]);
nlv[j + n - i + 1] = max(nlv[j + n - i + 1], v[i][j]);
nrv[i + j] = max(nrv[i + j], v[i][j]);
}
}
}
for (int i = 1; i <= n + m; i++) {
plv[i] = max(plv[i], plv[i - 1]);
nrv[i] = max(nrv[i], nrv[i - 1]);
}
for (int i = n + m; i >= 1; i--) {
prv[i] = max(prv[i], prv[i + 1]);
nlv[i] = max(nlv[i], nlv[i + 1]);
}
for (int i = 1; i <= n + m - 1; i++) ans = max({ans, plv[i] + nlv[i + 1], nrv[i] + prv[i + 1]});
cout << ans << '\n';
return 0;
}
E. 最大异或
首先考虑 \(3\) 种特殊情况:
- \(S\) 中的所有位都是 \(0\)。此时答案显然为 \(0\)。
- \(S\) 中的所有位都是 \(1\)。根据异或的定义,答案为 \(S\) 是不可能的,因为需要有 \(0\) 才能使所有位都不变。根据贪心的思想,答案要尽可能大需要让高位(靠左侧的位)尽可能大,所以将 \(S\) 与 \(1\) 求异或使得最低位变为 \(0\)。
- \(S\) 形如 \(00000011111\),即高位均为 \(0\) 低位均为 \(1\)。因为不可能让原本为 \(0\) 的位变为 \(1\),所以此时答案不可能大于 \(S\)。于是将 \(S\) 与 \(0\) 求异或使得答案为 \(S\)。
现在考虑一般情况。上面的特殊情况启示了使用贪心法解决这道题,即尽可能使高位为 \(1\)。由此可以看出 \(s_1\) 与 \(s_2\) 必然有其一是 \(S\)。为了方便讨论,不妨设 \(s_1=S\)。
注意到,刚才的贪心思路等价于,使从左到右的第一个 \(0\) 尽可能靠右。因此要尽可能填补 \(s_1\) 中的 \(0\)。
设去掉前导 \(0\) 后,第一次出现的连续 \(1\) 个数为 \(x\),第一次出现的连续 \(0\) 个数为 \(y\)。显然这 \(y\) 个 \(0\) 只能用 \(x\) 个 \(1\) 中的一些填补。如果 \(x \le y\),则用 \(x\) 个 \(1\) 填补 \(y\) 个 \(0\) 中所在位更高的 \(x\) 个即可;反之,则应将多余的 \(y-x\) 个 \(1\) 去掉,防止将后面的 \(1\) 变为 \(0\)。
综上,需要找到从左到右第一个连续的 \(\min(x,y)\) 个 \(1\) 作为 \(s_2\) 的起始部分。接下来即可直接得到完整的 \(s_2\)。这个方法的时间复杂度为 \(\mathcal{O}(n)\)。
#include <bits/stdc++.h>
using namespace std;
string s;
char t[10000007];
int n;
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int _;
cin >> _;
while (_--) {
cin >> n >> s;
s = "#" + s;
bool all0 = true, all1 = true;
for (int i = 1; i <= n; i++) {
if (s[i] == '0') all1 = false;
if (s[i] == '1') all0 = false;
}
if (all0) {
cout << "0\n";
continue;
} else if (all1) {
for (int i = 1; i < n; i++) cout << '1';
cout << "0\n";
continue;
}
int p = 1;
for (int i = 1; i <= n; i++) if (s[i] == '1') {p = i; break;}
int q = -1;
for (int i = p + 1; i <= n; i++) if (s[i] == '0') {q = i; break;}
if (q == -1) {
for (int i = p; i <= n; i++) cout << '1';
cout << '\n';
continue;
}
int r;
for (r = q; r <= n; r++) {
if (s[r] == '1') break;
}
int x = q - p, y = r - q;
int cnt = min(x, y);
int la = p, ra = n, lb = q - cnt, rb = n - cnt;
for (int i = ra, j = rb, k = ra - la + 1; i >= la && j >= lb && k >= 1; i--, j--, k--) t[k] = (s[i] != s[j]) ? '1' : '0';
for (int i = ra - (rb - lb + 1), k = ra - la + 1 - (rb - lb + 1); i >= la && k >= 1; i--, k--) t[k] = s[i];
for (int i = 1; i <= n - p + 1; i++) cout << t[i];
cout << '\n';
}
return 0;
}
F. 拔树游戏
注意到根节点的权值只和它的儿子有关,所以可以想到维护一个小根堆,存储根节点儿子的所有权值。但这样做更新答案时间复杂度过高。
不难发现,可以将选出的子节点的所有儿子都直接加入到堆里。这个操作和原来的方法是等价的。
证明
- 一步要选的结点在选出子节点的儿子里,则这样操作显然是正确的。
- 一步要选的结点在根节点的儿子里,则新加的这些节点不会影响选值。所以这样操作仍然是正确的。
综上,这样操作是正确的。
#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 5;
struct node {
int x, id;
};
const bool operator < (const node &x, const node &y) {return x.x > y.x;}
int n, a[N];
vector<int> g[N];
priority_queue<node> q;
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n;
for (int i = 2; i <= n; i++) {
int fa;
cin >> fa;
g[fa].push_back(i);
}
for (int i = 1; i <= n; i++) cin >> a[i];
q.push({a[1], 1});
for (int t = 1; t <= n; t++) {
cout << q.top().x << '\n';
int u = q.top().id;
q.pop();
for (int i = 0; i < g[u].size(); i++) {
int v = g[u][i];
q.push({a[v], v});
}
}
return 0;
}
回顾
这场模拟赛提示了一些平常学习注意不到的问题,如:
- 时间分配不合理
- 错误判断需要使用的知识点
- 对题目难度和用时估计不准
希望下次比赛注意。
本文来自博客园,作者:cwkapn,转载请注明原文链接:https://www.cnblogs.com/cwkapn/p/18965634


浙公网安备 33010602011771号