牌线:4 / 5 快 / 7 / 10

喜欢 KM。

VP 反思:

  • 读题时边读边写关键题意,不要读完后凭感觉回忆题目,全是错的。

  • 如果输出复杂的题 WA 了,仔细列出可能哪个地方挂了,Yes No 判错了还是输出数量不对还是修改的位置不对,一一检查。

B. Beautiful Dangos(二分,分类讨论)【Hard】

二分 最优长度(当然可以通过双指针或者其它技巧省略二分)。

有了最优长度就可以遍历所有O(n)\mathcal{O}(n) 个可行区间, 对每个区间 check

check 的充分必要条件是 对于三个字符,每个字符单独放的时候都合法 。(如果忽略边上两个,那么只需要出现次数最多的那个不超过 n/2。考虑边上两个也就加减 eps 的区别,还是 n/2 左右。如果最多的那个能放,那剩下两个也就都能放。考虑到会有并列最多,check 3 个比较好。)

现在得到了最优区间以及一组可行端点。具体填法还需要做一些分类讨论,代码写得特别丑不过为了完整性还是放上来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

#define endl '\n'

constexpr int inf = 0x3f3f3f3f;

const string CWP = "CWP";

void solve() {
int n;
cin >> n;

string s;
cin >> s;

for (auto& c : s) {
c = CWP.find(c);
}

int l = n, r = -1;
for (int i = 1; i < n; i++) {
if (s[i] == s[i - 1]) {
l = min(l, i);
r = max(r, i - 1);
}
}

if (l == n && r == -1) {
cout << "Beautiful" << endl;
return;
}

vector<array<int, 3>> pre(n + 1);
for (int i = 0; i < n; i++) {
pre[i + 1] = pre[i];
pre[i + 1][s[i]]++;
}

if (ranges::max(pre[n]) > (n + 1) / 2) {
cout << "Impossible" << endl;
return;
}

cout << "Possible" << endl;

array<int, 3> res{inf, inf, inf}; // len, L, R
auto solve = [&](int l, int r) {
auto check = [&](int len) {
// [l, r] subset [L, R]
for (int L = 0; L + len - 1 < n; L++) {
int R = L + len - 1;
if (L <= l && r <= R) {
bool ok = true;
array<int, 3> cnt{};
for (int o = 0; o < 3; o++) {
cnt[o] = pre[R + 1][o] - pre[L][o];
}
for (int o = 0; o < 3; o++) {
int x = 0;
x += L > 0 && o == s[L - 1];
x += R < n - 1 && o == s[R + 1];
if (cnt[o] > (len + 1 - x) / 2) {
ok = false;
}
}
if (ok) {
return array{len, L, R};
}
}
}
return array{inf, inf, inf};
};
int lo = r - l + 1;
int hi = n + 1;
while (lo < hi) {
int mid = lo + hi >> 1;
if (check(mid) != array{inf, inf, inf}) {
hi = mid;
} else {
lo = mid + 1;
}
}
res = min(res, check(lo));
};
if (l != r + 1) {
solve(l, r);
} else {
solve(l, l);
solve(r, r);
}

int L = res[1];
int R = res[2];

assert(L < n && R < n);
cout << L + 1 << " " << R + 1 << endl;

array<int, 3> cnt{};
for (int o = 0; o < 3; o++) {
cnt[o] = pre[R + 1][o] - pre[L][o];
}

for (auto seq : {array{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 1, 0}, {2, 0, 1}}) {
for (int op = 0; op < 2; op++) {
vector<int> vis(n);
bool ok = true;
for (int j = 0; j < 3; j++) {
int o = seq[j];
if (!ok) {
break;
}
if (op ? j == 0 : j) {
int i = L;
for (int _ = 0; _ < cnt[o]; _++) {
while (i <= R && vis[i]) {
i++;
}
if (i == R + 1) {
ok = false;
break;
}
vis[i] = true;
s[i] = o;
i += 2;
}
} else {
int i = R;
for (int _ = 0; _ < cnt[o]; _++) {
while (i >= L && vis[i]) {
i--;
}
if (i == L - 1) {
ok = false;
break;
}
vis[i] = true;
s[i] = o;
i -= 2;
}
}
}
if (!ok) {
continue;
}

int l = n, r = -1;
for (int i = 1; i < n; i++) {
if (s[i] == s[i - 1]) {
l = min(l, i);
r = max(r, i - 1);
}
}
if (l == n && r == -1) {
for (auto& c : s) {
c = CWP[c];
}
cout << s << endl;
return;
}
}
}
assert(false);
return;
}

signed main() {
cin.tie(nullptr)->sync_with_stdio(false);

int t;
cin >> t;

while (t--) {
solve();
}

return 0;
}

F. Follow the Penguins(模拟)【Medium】

找到最快追上导师的人,计算其答案,并重新计算以它为导师的人的时间。重复这个过程,直到每个人都追上其导师。

用堆加速,存储(追上导师的时间,人),复杂度是O(nlogn)\mathcal{O}(n\log n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

#define endl '\n'

constexpr ll inf = 0x3f3f3f3f3f3f3f3f;

signed main() {
cin.tie(nullptr)->sync_with_stdio(false);

int n;
cin >> n;

vector<int> target(n);
vector<vector<int>> adj(n);
for (int i = 0; i < n; i++) {
cin >> target[i];
target[i]--;
adj[target[i]].push_back(i);
}

vector<int> pos(n);
for (int i = 0; i < n; i++) {
cin >> pos[i];
pos[i] *= 2;
}

vector<int> dir(n);
for (int i = 0; i < n; i++) {
if (pos[target[i]] < pos[i]) {
dir[i] = -1;
} else {
dir[i] = 1;
}
}

set<pair<int, int>> s;
for (int x = 0; x < n; x++) {
if (dir[x] != dir[target[x]]) {
s.insert({ abs(pos[target[x]] - pos[x]) / 2, x }); // 追上导师的时间,人
}
}

vector<ll> res(n, -1);
vector<ll> npos(n);
while (!s.empty()) {
auto [time, x] = *s.begin();
s.erase(s.begin());

if (res[x] != -1) {
continue;
}
res[x] = time;
npos[x] = pos[x] + res[x] * dir[x];

for (auto y : adj[x]) {
s.erase({ abs(pos[target[y]] - pos[y]) / 2, y });
s.insert({ abs(npos[x] - pos[y]), y });
}
}
for (auto x : res) {
cout << x << " ";
}
cout << endl;

return 0;
}

G. Grand Voting【Easy】

读错题 WA 一发,写错了又 WA 一发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <bits/stdc++.h>
using namespace std;

int main() {
cin.tie(nullptr)->sync_with_stdio(false);

int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
sort(a.begin(), a.end());
int s = 0;
for (int i = 0; i < n; i++) {
if (s >= a[i]) {
s++;
} else {
s--;
}
}
int maxx = s;
s = 0;
for (int i = n - 1; i >= 0; i--) {
if (s >= a[i]) {
s++;
} else {
s--;
}
}
int minn = s;

cout << maxx << " " << minn << endl;

return 0;
}

I. Imagined Holly(树,思维)【Medium】

类似于给定u,vu,v,求树上uvu-v 点权异或和那种题。本题中,Au,v=A1,uA1,vlca(u,v)A_{u,v}=A_{1,u}\oplus A_{1,v}\oplus \operatorname{lca}(u,v)。这就直接得到了每两个点的 lca。

再依据这个关系 拓扑排序 就能知道树的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

#define endl '\n'

int main() {
cin.tie(nullptr)->sync_with_stdio(false);

int n;
cin >> n;

vector a(n, vector<int>(n));
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
cin >> a[i][j];
a[j][i] = a[i][j];
}
}

vector<vector<int>> adj(n);
for (int u = 0; u < n; u++) {
for (int v = u; v < n; v++) {
int lca = a[u][v] ^ a[0][u] ^ a[0][v];
lca--;
if (lca != u) {
adj[lca].push_back(u);
}
if (lca != v) {
adj[lca].push_back(v);
}
}
}

vector<int> deg(n);
for (int x = 0; x < n; x++) {
for (auto y : adj[x]) {
deg[y]++;
}
}
queue<int> Q;
for (int x = 0; x < n; x++) {
if (deg[x] == 0) {
Q.push(x);
}
}

vector<pair<int, int>> res;
while (!Q.empty()) {
auto x = Q.front();
Q.pop();
for (auto y : adj[x]) {
if (--deg[y] == 0) {
res.push_back({x, y});
Q.push(y);
}
}
}

for (auto [x, y] : res) {
cout << x + 1 << " " << y + 1 << endl;
}

return 0;
}

J. January’s Color(树形 DP)【Medium】

最优的过程是:先间接得到xx 的兄弟,将他们合并得到父节点xx',再间接得到xx' 的兄弟,将他们合并得到父节点xx'',如此反复,直至得到yy

所以需要预处理三个东西:

  • 间接得到xx 的最小代价ff
  • 间接得到xx 的兄弟的最小代价gg
  • 询问x 1x \ 1 的答案g\sum g

这些都可以在树上 DFS 时求得,复杂度O(nlogn)\mathcal{O}(n\log n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

#define endl '\n'

constexpr ll inf = 0x3f3f3f3f3f3f3f3f;

int main() {
cin.tie(nullptr)->sync_with_stdio(false);

int t;
cin >> t;

while (t--) {
int n, q;
cin >> n >> q;

vector<ll> c(n);
for (int i = 0; i < n; i++) {
cin >> c[i];
}

vector<vector<int>> adj(n);
for (int i = 1; i < n; i++) {
int x, y;
cin >> x >> y;
x--;
y--;
adj[x].push_back(y);
adj[y].push_back(x);
}

vector<int> parent(n, -1);
vector<int> dfn(n), dfm(n), ord;
auto dfs0 = [&](this auto self, int x) -> void {
if (parent[x] != -1) {
adj[x].erase(find(adj[x].begin(), adj[x].end(), parent[x]));
}
dfn[x] = ord.size();
ord.push_back(x);
for (auto y : adj[x]) {
parent[y] = x;
self(y);
}
dfm[x] = ord.size();
};
dfs0(0);

vector<ll> f(n); // get himself
vector<ll> g(n); // get his parent
auto dfs1 = [&](this auto self, int x) -> void {
f[x] = c[x];
if (adj[x].empty()) {
return;
}
int m = adj[x].size();
vector<ll> v;
for (auto y : adj[x]) {
self(y);
v.push_back(f[y]);
}
auto pre = v, suf = v;
for (int i = 1; i < m; i++) {
pre[i] = min(pre[i], pre[i - 1]);
}
for (int i = m - 2; i >= 0; i--) {
suf[i] = min(suf[i], suf[i + 1]);
}
for (int i = 0; i < m; i++) {
int y = adj[x][i];
g[y] = inf;
if (i > 0) {
g[y] = min(g[y], pre[i - 1]);
}
if (i < m - 1) {
g[y] = min(g[y], suf[i + 1]);
}
}
sort(v.begin(), v.end());
f[x] = min(f[x], v[0] + v[1]);
};
dfs1(0);

vector<ll> sumg(n);
auto dfs2 = [&](this auto self, int x) -> void {
for (auto y : adj[x]) {
sumg[y] = g[y] + sumg[x];
self(y);
}
};
dfs2(0);

auto is_ancester = [&](int x, int y) {
return dfn[x] <= dfn[y] && dfn[y] < dfm[x];
};

while (q--) {
int x, y;
cin >> x >> y;
x--;
y--;
if (!is_ancester(y, x)) {
cout << -1 << endl;
} else {
cout << sumg[x] - sumg[y] << endl;
}
}
}

return 0;
}

K. Killing Bits(图论建模,网络流)【Expert】

【题意】给定两个长度为nn 的序列a,b (0ai,bin1)a,b \ (0 \leqslant a_{i},b_{i} \leqslant n-1),每次可以选择一个 permutationp (0pin1)p \ (0 \leqslant p_{i} \leqslant n-1),置aiai&pia_{i} \gets a_{i} \operatorname{\&} p_{i},问能否操作多次将aa 变成bbn5×104n \leqslant 5\times 10^{4}

非常妙妙题。

先说结论:特判掉 trivial cases 后,序列至少操作一次且满足ai&bi=bia_{i} \operatorname{\&} b_{i}=b_{i},只需要检查是否存在一个 permutationpp,满足bi&pi=bib_{i}\operatorname{\&}p_{i}=b_{i} 即可。

必要性是显然的,如果用pp 操作得到的bb 是合法的结果,必然满足bi&pi=bib_{i}\operatorname{\&}p_{i}=b_{i}。下面说明充分性,需要证明若pp 满足bi&pi=bib_{i}\operatorname{\&}p_{i}=b_{i},则必然存在一种操作方法,能将aa 变成bb。对于每个ii 分别考虑。若bi=pib_{i}=p_{i},则aibia_{i}\gets b_{i} 已经成立。否则设jj 满足bi=pjb_{i}=p_{j},我们取 permutationqq 满足qi=pj, qj=piq_{i}=p_{j},\ q_{j}=p_{i}。一方面,用qq 操作后,aiai&qi    aiai&bia_{i}\gets a_{i} \operatorname{\&} q_{i}\iff a_{i}\gets a_{i} \operatorname{\&} b_{i} 得到了我们想要的结果;另一方面,ajai&qja_{j}\gets a_{i} \operatorname{\&} q_{j} 不会有副作用,已知bi&pi=bi    pj&qj=pjb_{i}\operatorname{\&}p_{i}=b_{i}\iff p_{j}\operatorname{\&}q_{j}=p_{j},且已知bj&pj=bjb_{j}\operatorname{\&}p_{j}=b_{j},所以bj&qj=bjb_{j}\operatorname{\&}q_{j}=b_{j},在操作ajai&qja_{j}\gets a_{i} \operatorname{\&} q_{j} 之后仍然满足aj&bj=bja_{j} \operatorname{\&} b_{j}=b_{j},没有影响。

这个东西可以用网络流判断:

  • 源点连bib_{i},容量为 1;
  • iii2ki \mid 2^{k},容量为++\infty,表示 AND 运算;
  • ii 连汇点,容量为 1。

判断最大流是否为nn 即可。

边数O(nlogn)\mathcal{O}(n\log n),考虑到图结构,复杂度估计是不超过二分图匹配的复杂度O(MN)=O(nnlogn)\mathcal{O}(M\sqrt{ N}) = \mathcal{O}(n \sqrt{ n} \log n),可以通过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void solve() {
int n;
cin >> n;

vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}

vector<int> b(n);
for (int i = 0; i < n; i++) {
cin >> b[i];
}

for (int i = 0; i < n; i++) {
if ((a[i] & b[i]) != b[i]) {
cout << "No\n";
return;
}
}

if (a == b) {
cout << "Yes\n";
return;
}

int s = n;
int t = s + 1;
MaxFlow maxflow(t + 1);
for (int i = 0; i < n; i++) {
maxflow.add(s, b[i], 1);
maxflow.add(i, t, 1);
for (int bit = 0; bit < 20; bit++) {
if (i >> bit & 1) {
maxflow.add(i ^ (1 << bit), i, inf);
}
}
}

auto flow = maxflow.dinic(s, t);
if (flow == n) {
cout << "Yes\n";
} else {
cout << "No\n";
}
return;
}

L. Let’s Make a Convex!(贪心,双指针)【Easy】

充要条件: 除最长边以外的边长和>> 最长边

最长边确定后,可以从比它小的边中贪心选k1k-1 条长边,组成多边形的其它边。也就是说, 选出的边是连续区间

于是得到这样的过程:首先取最长的那条边(记为rr),并选取其它所有边(记最小的为ll),判断是否合法。如果合法,那么记录k=rlk=r-l 的答案,并令ll+1l\gets l+1;如果不合法,说明需要调小最大边,令rr1r\gets r-1。模拟即可。

双指针写越界 WA 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

#define endl '\n'

int main() {
cin.tie(nullptr)->sync_with_stdio(false);

int t;
cin >> t;

while (t--) {
int n;
cin >> n;

vector<ll> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
sort(a.begin(), a.end());

vector<ll> pre(n + 1);
for (int i = 0; i < n; i++) {
pre[i + 1] = pre[i] + a[i];
}

int l = 0;
int r = n - 1;
vector<ll> ans(n);
while (l <= r) {
while (l > 0 && pre[r] - pre[l] <= a[r]) {
l--;
r--;
}
if (pre[r] - pre[l] > a[r]) {
ans[r - l] = pre[r] - pre[l] + a[r];
l++;
} else {
r--;
}
}

for (auto x : ans) {
cout << x << " ";
}
cout << endl;
}

return 0;
}

M. Mystique as Iris(状态机 DP)【Expert】

【题意】给定一个长度为nn 的序列aa,问有多少种方案把1-1 替换为1m1\sim m 的数,使得此序列可以通过若干次操作,每次可以选择一个两个相邻的数x,yx,y,置xx1, y0x\gets x-1,\ y\gets0,然后删除所有的 0,最终将序列变空。n106, m108n \leqslant 10^{6},\ m \leqslant 10^{8}

妙妙题。

合法序列有三种:有22n1n-1 中任意一个数;头或尾是 1;有连续两个 1。此外全 1 且奇数需要特判一下。

现在,本质不同的数只有三种:112n12\sim n-1nmn\sim m

我们直接 DP 求解方案,考虑一个三个状态的状态机,状态 0 表示当前序列末尾是 1,状态 1 表示当前序列末尾是nmn\sim m,状态 2 表示当前序列已经合法。只需要考虑一下从一个状态输入一个字符后到哪一个状态,以及这个转移的系数即可。

初始状态是 0,接受状态是 0 / 2。

复杂度O(n)\mathcal{O}(n)

![[…/Untitled/img/ce187b85667f0576573edf0b3e6cff5a_720.png]]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void solve() {
int n, m;
cin >> n >> m;

array<Z, 3> f{1, 0, 0};
bool allone = n & 1;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
if (x != -1 && x != 1) {
allone = false;
}
array<Z, 3> nf;
if (x == -1 || x == 1) {
nf[2] += f[0];
nf[0] += f[1];
nf[2] += f[2];
}
if (x == -1 || x >= n) {
Z coef = x == -1 ? max(0, m - n + 1) : 1;
nf[1] += coef * f[0];
nf[1] += coef * f[1];
nf[2] += coef * f[2];
}
if (x == -1 || 2 <= x && x <= n - 1) {
Z coef = x == -1 ? (min(m, n - 1) - 1) : 1;
nf[2] += coef * f[0];
nf[2] += coef * f[1];
nf[2] += coef * f[2];
}
f = nf;
}
cout << (f[0] + f[2] - allone) << endl;
}