AtCoder Regular Contest 194 (Div. 2)
ARC194A Operations on a Stack
发现一定是删掉若干段偶数长度的不选,直接 DP 即可做到线性。
void slv() {
int n = Read<int>();
vector<int> A(n);
for (int i = 0; i < n; i ++) {
Read(A[i]);
}
vector<array<ll, 3>> f(n);
f[0][2] = A[0], f[0][1] = 0, f[0][0] = -inf;
for (int i = 1; i < n; i ++) {
f[i][2] = A[i] + max(f[i - 1][0], f[i - 1][2]);
f[i][1] = max(f[i - 1][0], f[i - 1][2]);
f[i][0] = f[i - 1][1];
}
Write(max(f[n - 1][0], f[n - 1][2]), '\n');
return;
}
ARC194B Minimum Cost Sort
考虑下界。
对于每个数 \(i\),如果存在逆序对 \((j, i)\),那么就至少需要和 \(j\) 交换一次.,那么下界就是一个等差数列求和。
记 \(c_i\) 表示 \(i\) 位置的逆序对个数,不难得到下界为:
用一种你喜欢的方式求出逆序对后计算即可。
void slv() {
int n = Read<int>();
ll ans = 0; Fenwick<int> tr(n + 1);
for (int i = 1; i <= n; i ++) {
int x = Read<int>();
int s = tr.Query(n - x + 1);
ans += 1LL * ((i - s) + (i - 1)) * ((i - 1) - (i - s) + 1) / 2;
tr.Update(n - x + 1, 1);
}
Write(ans, '\n');
return;
}
ARC194C Cost to Flip
一定是先把一些 \(1\) 变成 \(0\),然后再把 \(0\) 变成 \(1\)。
其中第一步操作的一定是所有的 \(A_i = 1, B_i = 0\) 位置和一部分 \(A_i = B_i = 1\) 的位置,第二步操作的一定是所有的 \(A_i = 0, B_i = 1\) 的位置和在第一步中操作过的 \(A_i = B_i = 1\) 的位置。
通过进一步的观察,发现对于 \(A_i = B_i = 1\) 的位置,我们只会改按 \(C_i\) 从大到小排序后的一段前缀。
然后就枚举这段前缀随便统计一下答案啥的,反正细节很多,烦。
void slv() {
int n = Read<int>();
vector<int> A(n), B(n), C(n);
for (int i = 0; i < n; i ++) {
Read(A[i]);
}
for (int i = 0; i < n; i ++) {
Read(B[i]);
}
for (int i = 0; i < n; i ++) {
Read(C[i]);
}
vector<int> pA, pB, pC;
for (int i = 0; i < n; i ++) {
if (A[i] == 1 && B[i] == 0) {
pA.emplace_back(i);
} else if (A[i] == 0 && B[i] == 1) {
pB.emplace_back(i);
} else if (A[i] == 1 && B[i] == 1) {
pC.emplace_back(i);
}
}
sort(pA.begin(), pA.end(), [&](int i, int j) { return C[i] > C[j]; });
sort(pB.begin(), pB.end(), [&](int i, int j) { return C[i] < C[j]; });
sort(pC.begin(), pC.end(), [&](int i, int j) { return C[i] > C[j]; });
const int nA = pA.size(), nB = pB.size(), nC = pC.size();
vector<ll> f(1 + nC);
{
ll s = 0;
for (int i = 0; i < n; i ++)
if (A[i] == 1) s += C[i];
for (int i = 0; i < nA; i ++) {
s -= C[pA[i]], f[0] += s;
}
s = 0;
for (int i = 0; i < n; i ++)
if (A[i] == 1) s += C[i];
for (int i = 1, j = 0; i <= nC; i ++) {
while (j < nA && C[pA[j]] > C[pC[i - 1]]) s -= C[pA[j ++]];
s -= C[pC[i - 1]];
f[i] = f[i - 1] - 1LL * C[pC[i - 1]] * (nA - j) + s;
}
};
vector<ll> g(1 + nC);
{
ll s = 0;
for (int i = 0; i < n; i ++)
if (B[i] == 1 && A[i] == 1) s += C[i];
for (int i = 0; i < nB; i ++) {
s += C[pB[i]], g[0] += s;
}
s = 0;
for (int i = 0; i < n; i ++)
if (B[i] == 1) s += C[i];
for (int i = 1, j = nB - 1; i <= nC; i ++) {
while (j >= 0 && C[pB[j]] > C[pC[i - 1]]) s -= C[pB[j --]];
g[i] = g[i - 1] + s - 1LL * C[pC[i - 1]] * (j + 1);
s -= C[pC[i - 1]];
}
};
ll ans = 1E18;
for (int i = 0; i <= nC; i ++)
cmin(ans, f[i] + g[i]);
Write(ans, '\n');
return;
}
ARC194D Reverse Brackets
建括号树,操作就是选一段连续的儿子区间,然后将 这段区间 和 其所有后代的儿子 reverse 一下。
同时还要对后代操作非常烦,但是可以通过对所有儿子操作一次变成 reverse 一段连续的儿子区间。
那么现在要做的就是每次可以 reverse 一段连续的儿子区间,问能生成多少棵本质不同的树。
考虑 DFS 计算答案,对于一个点,如果直接算儿子的方案数的乘积然后任意排列的话会算重。
进一步分析,发现不同构的子树是不互相影响的,同构的子树可以钦定必须是从左到右的顺序,这样就不会算重了。
暴力模拟上面的过程是 \(O(n^2)\) 的。
constexpr int N = 5e3 + 5;
char s[N];
void slv() {
int n; Read(n), Read(s + 1);
vector<int> sum(n + 1);
for (int i = 1; i <= n; i ++) {
sum[i] = (s[i] == '(' ? 1 : -1);
}
partial_sum(sum.begin(), sum.end(), sum.begin());
int cur = 0;
map<vector<int>, int> id;
id[vector<int>()] = 0;
auto ID = [&](const vector<int> &v) {
if (!id.count(v)) {
id[v] = ++ cur;
}
return id[v];
};
auto Solve = [&](auto self, int L, int R) -> pair<int, mint> {
if (L > R) {
return {0, 1};
}
vector<pair<int, mint>> son;
for (int l = L, r; l <= R; l = r + 1) {
r = l;
while (sum[++ r] != sum[l - 1]);
son.emplace_back(self(self, l + 1, r - 1));
}
sort(son.begin(), son.end());
const int n = son.size();
mint ans = 1;
for (int l = 0, r; l < n; l = r + 1) {
r = l, ans *= son[l].sec;
while (r + 1 < n && son[r + 1].fir == son[l].fir) {
ans *= son[++ r].sec;
}
ans *= comb.C(r + 1, r - l + 1);
}
vector<int> hsh(n);
for (int i = 0; i < n; i ++)
hsh[i] = son[i].fir;
return {ID(hsh), ans};
};
Write((int)Solve(Solve, 1, n).sec, '\n');
return;
}
ARC194E Swap 0^X and 1^Y
对于一段连续的 \(1\),将它分成若干段 \(= X\) 的 \(A\) 和 \(< X\) 个 \(1\),\(0\) 同理分成 \(B\) 和 \(0\)。
不难发现,如果我们允许 \(A\) 和 \(1\),\(B\) 和 \(0\),\(A\) 和 \(B\) 之间的交换,那么这个和原问题是没有区别的,因为不会发生初始不同的 \(0 / 1\) 的连续段之间的拼接。
发现此时 \(0\) 限制住了 \(1\) 和 \(A\) 的移动,\(1\) 限制住了 \(0\) 和 \(B\) 的移动,所以直接把相邻 \(0\) 之间 \(1\) 的个数、相邻 \(0\) 之间 \(A\) 的个数,相邻 \(1\) 之间 \(B\) 的个数这三个东西看成特征值,特征值相等的序列一定能互相转化。
求出 \(S\) 和 \(T\) 的特征值之后进行判定即可,时间复杂度线性。
constexpr int N = 5E5 + 5;
char s[N], t[N];
void slv() {
int N, X[2];
Read(N, X[0], X[1]);
vector<int> A(N), B(N);
Read(s), Read(t);
for (int i = 0; i < N; i ++) {
A[i] = s[i] - '0';
B[i] = t[i] - '0';
}
auto calc = [&](const vector<int> &a, int x, int y) {
const int n = a.size();
vector<int> ans; int cnt = 0;
for (int i = 0; i < n; i ++) {
if (a[i] == x) {
++ cnt;
} else if (a[i] == y) {
ans.emplace_back(cnt);
cnt = 0;
}
}
ans.emplace_back(cnt);
return ans;
};
auto trans = [&](const vector<int> &a) -> tuple<vi, vi, vi> {
const int n = a.size();
vector<int> b;
for (int l = 0, r; l < n; l = r + 1) {
r = l; int o = a[l];
while (r + 1 < n && a[r + 1] == a[l]) {
++ r;
}
int len = r - l + 1;
while (len >= X[o]) {
b.emplace_back(2 + o);
len -= X[o];
}
while (len) {
b.emplace_back(o);
-- len;
}
}
return {calc(b, 0, 1), calc(b, 0, 3), calc(b, 1, 2)};
};
Yes(trans(A) == trans(B));
return;
}