CF2244赛解
写在前面
比较有意思的div3,没有很臭重的思维题,算是非常小清新的比赛。还剩半个小时AK了,但 \(F\) 弱智了被罚了两罚,rk只有199.
CF2244A Iskander and Drawings
题意:找一个最长的#连续段输出长度除以 \(2\) 下取整。
题解:直接模拟即可。
#include <bits/stdc++.h>
using namespace std;
char s[12];
void solve() {
int n;
cin >> n >> s + 1;
int mx = 0, now = 0;
for (int i = 1; i <= n; ++i) {
if (s[i] == '*') mx = max(mx, now / 2 + now % 2), now = 0;
else ++now;
}
mx = max(mx, now / 2 + now % 2);
cout << mx << '\n';
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--) solve();
return 0;
}
CF2244B Nikita and Books
题意:给定一个序列,每次可以把其中一个位置的值选取任意大小移动到右边的位置,判定是否能通过操作将序列调整成单调增。
题解:我们发现每个位置的值只要大于等于其下标即可,我们贪心让每个位置的值恰好等于下标,留出尽量多的余量给下一个下标,最后把多余的给最后一个下标即可。
维护一个当前余量计数器如果不足则分配,超出就加给余量,如果余量时时刻刻大于等于 \(0\) 则有解,否则无解。
#include <bits/stdc++.h>
#define int long long
const int N = 2e5 + 1;
using namespace std;
int a[N];
void solve() {
int n, sum = 0;
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
for (int i = 1; i <= n; ++i) {
if (a[i] + sum < i) return cout << "NO\n", void();
sum += a[i] - i;
}
cout << "YES\n";
}
signed main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--) solve();
return 0;
}
CF2244C Stepan and Permutation
题意:给定一个排列,每次可以把一个位置的值和距离为 \(x\) 或 \(y\) 的交换,判断是否可以通过操作使排列有序。
题解:我们发现只要每个值与其应该所处的位置的距离需要通过若干步 \(x\) 和 \(y\) 的组合达到,中间多余的影响可以做一个反向的组合来抵消。
设某个数初始的位置与应处的位置的距离为 \(d\) 那么对于任意 \(d\) 而言 \(d=ax+by\ (a,b\in \mathbb{Z})\) 有解为问题有解的充要条件。
而数论告诉我们 \(d=ax+by\ (a,b\in \mathbb{Z})\) 有解的充要条件为 \(d\mid \gcd(a,b)\)。
枚举每个 \(d\) 逐一判断即可。
#include <bits/stdc++.h>
const int N = 2e5 + 1;
using namespace std;
int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }
int a[N];
void solve() {
int n, x, y;
cin >> n >> x >> y;
int d = gcd(x, y);
for (int i = 1; i <= n; ++i) cin >> a[i];
for (int i = 1; i <= n; ++i) if (abs(a[i] - i) % d) return cout << "NO\n", void();
cout << "YES\n";
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--) solve();
return 0;
}
CF2244D Yaroslav and Productivity
题意:给定一个序列和一个位置集合,每次操作可以选择集合中的一个位置 \(i\),将前 \(i\) 个元素正负反转,求在经过操作后最后序列所有数可能的和的最大值。
题解:我们把位置集合排序后发现后一次操作只与前一次操作的和,还有前一个位置到当前位置的之间的数有关。当前操作只决定上一次操作得到的前缀值是否取反,中间那坨和还未受到操作的数是否取反有关。显然无后效性,用dp解决这个问题。
-
如果不反转当前位置,当前操作位置的前缀最大和为上一次操作的前缀最大值和两个位置中间那坨数的和之和。
-
如果反转当前位置,当前操作位置的前缀最大和为上一次操作的前缀最小和的相反数与两个位置中间那坨数的和的相反数之和。
我们需要维护第 \(i\) 个位置的前缀最大和 \(f_i\),前缀最小和 \(g_i\),以及原数组前缀和 \(sum\)。
转移方程:
#include <bits/stdc++.h>
#define int long long
const int N = 2e5 + 1;
using namespace std;
int f[N], g[N], sum[N], a[N];
void solve() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; ++i) cin >> sum[i], sum[i] += sum[i - 1];
for (int i = 1; i <= m; ++i) cin >> a[i];
sort(a + 1, a + m + 1);
for (int i = 1; i <= m; ++i) {
f[i] = max(f[i - 1] + sum[a[i]] - sum[a[i - 1]], -g[i - 1] - sum[a[i]] + sum[a[i - 1]]);
g[i] = min(g[i - 1] + sum[a[i]] - sum[a[i - 1]], -f[i - 1] - sum[a[i]] + sum[a[i - 1]]);
}
cout << f[m] + sum[n] - sum[a[m]] << '\n';
}
signed main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--) solve();
return 0;
}
CF2244E Masha and the Garland
题意:给定一个 \(01\) 串和一些区间询问。一次操作可以选择一段连续的位置取反,对于每个区间求让区间任意相邻的数不相等的最小操作次数。
题解:注意到一次操作仅能使操作左端和左端左边的数,操作右端和操作右端的数的同异状态进行改变。
设 \(s_i=[a_i==a_{i-1}]\) 即每个位置是否与后一个位置相等。答案为 \(\lfloor\frac{\sum\limits_{i=l}^{r-1} s_i}{2}\rfloor\),前缀和预处理即可。
#include <bits/stdc++.h>
const int N = 2e5 + 1;
using namespace std;
int sum[N];
char s[N + 1];
void solve() {
int n, q;
cin >> n >> q >> s + 1;
for (int i = 1; i <= n; ++i) sum[i] = sum[i - 1] + (s[i] == s[i - 1]);
while (q--) {
int l, r, k;
cin >> l >> r >> k;
if ((sum[r] - sum[l]) / 2 + (sum[r] - sum[l]) % 2 <= k) cout << "YES\n";
else cout << "NO\n";
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--) solve();
return 0;
}
CF2244F Anya Loves Trees!
题意:给定一棵有根树,每个节点的儿子初始按编号大小从左到右排序,每个叶子上有一个数字,所有叶子上的数字构成一个排列。每次操作可以让一个节点的儿子顺序左循环一次。判断是否可以通过若干次操作使最后树的叶子上 的数字从左到右依次递增。
题解:左循环会发生什么?会把前缀若干个儿子构成的叶子区间移动到最后面。我们不能够把一个儿子对应的区间和另外一个儿子对应的区间拆分或合并。因为叶子上的数字构成排列,那么显然 每个儿子对应的叶子上数字的区间必须是连续的 。有了这个结论问题就变得简单了,通过简单的树形dp就能解决
记 \(f_i=(l,r)\) 为节点子树中的区间起始点与终止点构成的二元组。我们先判断每颗子树中的叶子数字是否能构成连续区间。我们把子树中的每一个 \(f_v\) 排序,判断前一个儿子的右端点是否是当前儿子的左端点 \(-1\) 即可。
当然别忘了,我们现在只保证了编号可能连续,我们还要仅通过左循环使编号连续。因为每个子树都是连续区间,且我们已经判断可能构成整体连续。无论多少次左循环操作等效的结果只能按顺序把一整段前缀放到最后面,那么有解的条件只能是有一段比较大的由子树区间构成的连续区间在一段比较小的子树构成的连续区间前面。这个问题等价于,有至多一个间断点使前一个儿子的右端点不等于后一个儿子的左端点 \(-1\) 。打一个 \(flag\) 遍历儿子过程中判定就行了。
时间复杂度 \(O(n\log n)\),空间复杂度 \(O(n)\)。事实上判断间断点数量的过程中顺便就能判断是否可能构成了,不需要单独把子树都拉下来进行排序依次多一个 \(\log n\),但需要记录的信息有点多,但别忘了最开始需要把儿子按编号从左到右排序已经多了一个 \(\log n\) 了所以也无所谓。说到这我一开始还因为忘了给儿子排序被罚了一发(qwq)。
#include <bits/stdc++.h>
const int N = 2e5 + 1;
using namespace std;
vector<int> e[N];
int a[N];
pair<int, int> f[N];
bool dfs(int x) {
if (e[x].empty()) {
f[x].first = f[x].second = a[x];
return true;
}
vector<pair<int, int> > q;
vector<pair<int, int> > w;
for (int y: e[x]) {
if (!dfs(y)) return false;
q.push_back(f[y]);
w.push_back(f[y]);
}
sort(w.begin(), w.end());
for (int i = 1; i < w.size(); ++i) if (w[i].first != w[i - 1].second + 1) return false;
bool flag = false;
for (int i = 1; i < q.size(); ++i) {
if (q[i].first != q[i - 1].second + 1) {
if (flag) return false;
flag = true;
}
}
f[x].first = w[0].first, f[x].second = w.back().second;
return true;
}
void solve() {
int n;
cin >> n;
for (int i = 1; i <= n; ++i) {
vector<int> empty;
swap(e[i], empty);
}
for (int i = 2; i <= n; ++i) {
int fa;
cin >> fa;
e[fa].push_back(i);
}
for (int i = 1; i <= n; ++i) sort(e[i].begin(), e[i].end());
for (int i = 1; i <= n; ++i) cin >> a[i];
if (dfs(1)) cout << "YES\n";
else cout << "NO\n";
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--) solve();
return 0;
}
CF2244G Yura and Deadlines
题意:给定一个序列可以选择若干个数,需要满足对于选取的任意两个 \(a_i,a_j\) 满足 \(|i-j|>\max(a_i,a_j)\),最大化选取的所有数的和。
题解:选取若干个数一眼 \(dp\),令 \(f_i\) 表示前 \(i\) 个点且选了第 \(i\) 个点的最大和。还有点对限制,点对问题要让形式尽量简单,有绝对值看上去不太好,按下标顺序遍历把绝对值消掉(我习惯dp里以 \(i\) 为大下标下文默认如此)。有 \(\max\) 不太好看,考虑拆成两个并立条件。
问题转化为:
我们把前面的点 \(j\) 的属性拆成 \(a_j+j\) 和 \(j\) 两个维度,那么对于 \(i\) 的转移就变成了二维平面最大值。因为是dp,离线有点麻烦,但看题解说是可以离线bit,赛时用主席树直接推过去了。第二个条件对应的限制比较简洁,用第二个条件为可持久化线段树的版本编号,前 \(j\) 颗线段树中的位置 \(p\) 维护前 \(j\) 每个 \(a_k+k=p\) 的 \(f_k\) 最大值。查询直接在第 \(a_i-i-1\) 个版本的主席树中找(满足第二个条件),小于 \(i\) 的最大值即可。时间复杂度 \(O(n\log n)\),空间复杂度 \(O(n\log n)\),离线可以把空间复杂度的 \(\log n\) 去掉。
这题还是比较板的,会二维数点做一点简单的转化还是能过的,不会的人就一点都不会。\(15\) 分钟主席树堆上去一发过了,没 \(F\) 题有意思。
#include <bits/stdc++.h>
#define int long long
const int N = 4e5 + 1;
using namespace std;
int root[N], ls[N * 20], rs[N * 20], tot, mx[N * 20], a[N], b[N];
void pushup(int p) { mx[p] = max(mx[ls[p]], mx[rs[p]]); }
void update(int &p, int rt, int lp, int rp, int pos, int val) {
p = ++tot;
if (lp == rp) {
mx[p] = max(mx[rt], val);
return;
}
int mid = (lp + rp) >> 1;
if (pos <= mid) rs[p] = rs[rt], update(ls[p], ls[rt], lp, mid, pos, val);
else ls[p] = ls[rt], update(rs[p], rs[rt], mid + 1, rp, pos, val);
pushup(p);
}
int query(int p, int lp, int rp, int l, int r) {
if (!p) return 0;
if (lp >= l && rp <= r) return mx[p];
int mid = (lp + rp) >> 1;
int ans = 0;
if (l <= mid) ans = query(ls[p], lp, mid, l, r);
if (r > mid) ans = max(ans, query(rs[p], mid + 1, rp, l, r));
return ans;
}
void solve() {
int n;
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i], b[i] = i;
for (int i = 1; i <= n; ++i) b[n + i] = a[i] + i;
sort(b + 1, b + n * 2 + 1);
int cnt = unique(b + 1, b + n * 2 + 1) - b - 1;
int ans = 0;
for (int i = 1; i <= n; ++i) {
int f = a[i];
if (i - a[i] > 1 && lower_bound(b + 1, b + cnt + 1, i) - b > 1)
f += query(root[i - a[i] - 1], 1, cnt, 1,
lower_bound(b + 1, b + cnt + 1, i) - b - 1);
update(root[i], root[i - 1], 1, cnt, lower_bound(b + 1, b + cnt + 1, i + a[i]) - b, f);
ans = max(ans, f);
}
cout << ans << '\n';
for (int i = 0; i <= n; ++i) root[i] = 0;
for (int i = 0; i <= tot; ++i) ls[i] = rs[i] = mx[i] = 0;
tot = 0;
}
signed main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--) solve();
return 0;
}

贪心,动态规划,线性dp,树形dp,可持久化线段树
浙公网安备 33010602011771号